From f1e22af484c3b7bc1c94069173f3f3f184290cd6 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Wed, 19 Nov 2025 12:56:49 -0600 Subject: [PATCH 001/145] Initial commit for rca-tool --- rca-tool/.github/workflows/deploy.yml | 76 + rca-tool/.gitignore | 7 + rca-tool/README.md | 140 + rca-tool/index.html | 1805 ++++++++++ rca-tool/liblzma-streaming-worker.js | 3175 +++++++++++++++++ .../liblzma-wasm/build-liblzma-streaming.sh | 64 + rca-tool/styles.css | 419 +++ 7 files changed, 5686 insertions(+) create mode 100644 rca-tool/.github/workflows/deploy.yml create mode 100644 rca-tool/.gitignore create mode 100644 rca-tool/README.md create mode 100644 rca-tool/index.html create mode 100644 rca-tool/liblzma-streaming-worker.js create mode 100755 rca-tool/liblzma-wasm/build-liblzma-streaming.sh create mode 100644 rca-tool/styles.css diff --git a/rca-tool/.github/workflows/deploy.yml b/rca-tool/.github/workflows/deploy.yml new file mode 100644 index 0000000..70b9253 --- /dev/null +++ b/rca-tool/.github/workflows/deploy.yml @@ -0,0 +1,76 @@ +name: Build and Deploy to GitHub Pages + +on: + push: + branches: [ main, master ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Emscripten + uses: mymindstorm/setup-emsdk@v14 + with: + version: 'latest' + + - name: Verify Emscripten installation + run: | + emcc --version + + - name: Build WASM module + run: | + cd liblzma-wasm + chmod +x build-liblzma-streaming.sh + ./build-liblzma-streaming.sh + + - name: Prepare deployment directory + run: | + mkdir -p _site + # Copy HTML, CSS and worker JS from root + cp index.html _site/ + cp styles.css _site/ + cp liblzma-streaming-worker.js _site/ + # Copy built WASM files + cp liblzma-wasm/dist-streaming/liblzma-xz-streaming.js _site/ + cp liblzma-wasm/dist-streaming/liblzma-xz-streaming.wasm _site/ + # Optional: Copy README if you want it accessible + cp README.md _site/ || true + + - name: List deployment files + run: | + echo "Files to be deployed:" + ls -lh _site/ + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: '_site' + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/rca-tool/.gitignore b/rca-tool/.gitignore new file mode 100644 index 0000000..63c96fd --- /dev/null +++ b/rca-tool/.gitignore @@ -0,0 +1,7 @@ +# Test results and artifacts +test-results/ +playwright-report/ +coverage/ + +# Playwright +playwright/.cache/ diff --git a/rca-tool/README.md b/rca-tool/README.md new file mode 100644 index 0000000..9e42b59 --- /dev/null +++ b/rca-tool/README.md @@ -0,0 +1,140 @@ +# RCA Tool + +A Root Cause Analysis (RCA) tool that analyzes cluster diagnostic files (hb_report and supportconfig) from .tar.gz and .tar.xz archives, with special support for SCC (Supportconfig) reports including cluster node detection and validation. + +## Features + +- **Client-Side Processing**: All decompression and analysis happens in your browser +- **Dual Format Support**: Handles both .tar.gz (gzip) and .tar.xz (LZMA) files +- **Streaming Decompression**: Memory-efficient processing of large files (>50MB) using liblzma +- **SCC Report Analysis**: Automatic detection and analysis of supportconfig reports +- **Cluster Node Detection**: Extracts cluster node names from ha.txt +- **Hosts File Validation**: Cross-validates cluster nodes against /etc/hosts entries +- **File Listing**: Shows complete contents of archives +- **No Server Dependencies**: Everything runs in the browser +- **Privacy-First**: No files sent to servers +- **Fast Performance**: Native WASM speed via Emscripten and Rust + +## Architecture + +### Decompression Engines +- **GZIP**: Rust flate2 library compiled to WASM +- **XZ/LZMA**: C library (liblzma from XZ Utils 5.4.6) compiled via Emscripten + - Streaming mode with 256KB chunks for large files + - Memory-efficient processing + - Located in: `liblzma-wasm/dist-streaming/` + +### UI Components +- **Drag-and-Drop**: Rust WASM (`pkg/`) +- **TAR Parsing & SCC Analysis**: JavaScript worker (`liblzma-streaming-worker.js`) +- **Rule-Based Analysis**: Extensible rule system for SCC report extraction + +## Prerequisites + +- Rust (install from [rustup.rs](https://rustup.rs/)) +- wasm-pack (will be installed automatically by the build script) +- Emscripten SDK (for rebuilding liblzma if needed) + +## Quick Start + +1. **Build the project:** + ```bash + ./build.sh + ``` + +2. **Serve the files:** + ```bash + # Using Python + python3 -m http.server 8000 + ``` + +3. **Open in browser:** + Navigate to `http://localhost:8000` + +## How to Use + +1. Open the web page in your browser +2. Drag a cluster diagnostic file (.tar.gz or .tar.xz) onto the drop zone +3. For SCC reports, view detected cluster nodes and validation results +3. The application will analyze the archive and display: + - File type detected (hb_report or supportconfig) + - Complete list of files in the archive + - Key diagnostic information + +## Supported File Types + +The application handles cluster diagnostic files: +- `.tar.gz` - Gzip compressed cluster files +- `.tar.xz` - XZ compressed cluster files +- **hb_report**: Heartbeat report files from cluster analysis +- **supportconfig**: SUSE support configuration files + +## Project Structure + +``` +wasm-hello/ +├── src/ +│ └── lib.rs # Main Rust code with WASM bindings and XZ/TAR processing +├── pkg/ # Generated WASM files (after build) +├── Cargo.toml # Rust dependencies and configuration +├── index.html # Web interface +├── build.sh # Build script +├── test-file.txt # Sample test file +└── README.md # This file +``` + +## How It Works + +1. **Rust WASM**: The `src/lib.rs` file contains: + - WASM bindings using `wasm-bindgen` + - Web API interactions through `web-sys` + - Dual compression support: XZ (lzma-rs) and Gzip (flate2) + - Custom TAR archive parsing and analysis + - Cluster file type detection (hb_report vs supportconfig) + - Drag and drop event handlers + - Complete client-side file processing + +2. **Pure Browser Solution**: Everything runs in the browser: + - No server-side dependencies + - No external JavaScript libraries + - No network requests for processing + - All decompression and analysis in WASM + +3. **Cluster File Detection**: Automatically identifies: + - **hb_report**: Heartbeat cluster reports + - **supportconfig**: SUSE support configuration files + - Complete file listing for diagnostics + +4. **Web Interface**: Clean, responsive interface for cluster file analysis + +## Dependencies + +- `wasm-bindgen`: Rust/WASM bindings +- `js-sys`: JavaScript API bindings +- `web-sys`: Web API bindings for DOM manipulation +- `lzma-rs`: Pure Rust XZ/LZMA decompression (WASM compatible) +- `flate2`: Gzip decompression support +- `wasm-bindgen-futures`: Async operations support + +## Building Manually + +If you prefer to build manually without the script: + +```bash +# Install wasm-pack if not already installed +curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + +# Build the WASM package +wasm-pack build --target web --out-dir pkg +``` + +## Browser Compatibility + +This project uses modern web APIs and requires a recent browser with: +- WebAssembly support +- File API support +- Drag and Drop API support + +## License + +This project is open source and available under the MIT License. \ No newline at end of file diff --git a/rca-tool/index.html b/rca-tool/index.html new file mode 100644 index 0000000..fa7bda1 --- /dev/null +++ b/rca-tool/index.html @@ -0,0 +1,1805 @@ + + + + + RCA Tool - WASM + + + + + +
+

RCA Tool

+ +
+

Instructions:

+
    +
  • Drag and drop a .tar.gz, .tar.xz, or .txz cluster file onto the drop zone below
  • +
  • The application will analyze and detect if it's an hb_report, supportconfig, or sosreport
  • +
  • View the complete file listing and diagnostic information
  • +
+
+ +
+
Loading WebAssembly module...
+
+
+ + +
+

Select Cluster File

+

Choose a .tar.gz or .tar.xz cluster file (hb_report, crm_report, supportconfig, or sosreport)

+

Supports both gzip and XZ compression

+ +
+

+    
+ + + + \ No newline at end of file diff --git a/rca-tool/liblzma-streaming-worker.js b/rca-tool/liblzma-streaming-worker.js new file mode 100644 index 0000000..5d97daf --- /dev/null +++ b/rca-tool/liblzma-streaming-worker.js @@ -0,0 +1,3175 @@ +// Streaming XZ decompression worker using liblzma +// Processes compressed data in chunks to keep memory usage low + +const CACHE_BUST = '?v=' + Date.now(); + +// Debug flag - will be set from main thread via message +// Can be true (all), false (none), or 'cluster' (cluster-related only) +let DEBUG_MODE = false; + +// Debug logging wrapper - only logs if DEBUG_MODE is true +function debugLog(...args) { + if (DEBUG_MODE === true) { + console.log(...args); + } else if (DEBUG_MODE === 'cluster') { + // Only log cluster-related messages + const firstArg = args[0]; + if (typeof firstArg === 'string' && + (firstArg.includes('cluster') || + firstArg.includes('Cluster') || + firstArg.includes('pacemaker') || + firstArg.includes('crm_mon') || + firstArg.includes('corosync') || + firstArg.includes('resource') || + firstArg.includes('Resource') || + firstArg.includes('fencing') || + firstArg.includes('STONITH'))) { + console.log(...args); + } + } +} + +// ============================================================================ +// SCC REPORT ANALYSIS RULES +// ============================================================================ +// Add new rules here to extract information from SCC/supportconfig reports +// Each rule defines which file to extract and how to parse it + +const SCC_RULES = { + // Rule: Detect if archive is an SCC report, hb_report, crm_report, or sosreport + detection: { + // Patterns to identify reports by filename + filenamePatterns: [ + /^scc_/, // SCC/supportconfig reports + /^nts_/, // NTS reports + /^hb_report/, // hb_report archives (older Pacemaker) + /^crm_report/, // crm_report archives (newer Pacemaker) + /^sosreport-/ // sosreport archives + ], + + // Check if filename matches any report pattern + isSCCReport: function(filename) { + return this.filenamePatterns.some(pattern => pattern.test(filename)); + } + }, + + // Rule: Extract Azure VM properties from instance_metadata.json + azureVMProperties: { + filePattern: /instance_metadata\.json$/, + + parse: function(content, filename) { + debugLog('[azureVMProperties parser] Analyzing Azure VM metadata in:', filename); + try { + const metadata = JSON.parse(content); + // Extract properties from root + const vmSize = metadata.vmSize || null; + const offer = metadata.offer || null; + const licenseType = metadata.licenseType || null; + debugLog('[azureVMProperties parser] VM Size:', vmSize); + debugLog('[azureVMProperties parser] Offer:', offer); + debugLog('[azureVMProperties parser] License Type:', licenseType); + return { + found: true, + vmSize: vmSize, + offer: offer, + licenseType: licenseType + }; + } catch (e) { + console.error('[azureVMProperties parser] Failed to parse JSON:', e); + return { found: false }; + } + } + }, + + // Rule: Extract OS release information from /etc/os-release + osRelease: { + filePattern: /\/(etc|usr\/lib)\/os-release$/, + + parse: function(content, filename) { + debugLog('[osRelease parser] Analyzing OS release information in:', filename); + const lines = content.split('\n'); + let name = null; + let version = null; + let versionId = null; + let prettyName = null; + + for (const line of lines) { + const trimmed = line.trim(); + + // Skip empty lines and comments + if (!trimmed || trimmed.startsWith('#')) continue; + + // Parse NAME="Distribution Name" + const nameMatch = trimmed.match(/^NAME=["']?([^"']+)["']?$/); + if (nameMatch) { + name = nameMatch[1]; + debugLog('[osRelease parser] Found NAME:', name); + } + + // Parse VERSION="Major.Minor (Codename)" or VERSION="Major.Minor" + const versionMatch = trimmed.match(/^VERSION=["']?([^"']+)["']?$/); + if (versionMatch) { + version = versionMatch[1]; + debugLog('[osRelease parser] Found VERSION:', version); + } + + // Parse VERSION_ID="Major.Minor" + const versionIdMatch = trimmed.match(/^VERSION_ID=["']?([^"']+)["']?$/); + if (versionIdMatch) { + versionId = versionIdMatch[1]; + debugLog('[osRelease parser] Found VERSION_ID:', versionId); + } + + // Parse PRETTY_NAME="Full Distribution Name with Version" + const prettyNameMatch = trimmed.match(/^PRETTY_NAME=["']?([^"']+)["']?$/); + if (prettyNameMatch) { + prettyName = prettyNameMatch[1]; + debugLog('[osRelease parser] Found PRETTY_NAME:', prettyName); + } + } + + // Extract major and minor version from VERSION_ID + let majorVersion = null; + let minorVersion = null; + if (versionId) { + const parts = versionId.split('.'); + majorVersion = parts[0] || null; + minorVersion = parts[1] || null; + } + + debugLog('[osRelease parser] Parsed OS info:', { + name: name, + version: version, + majorVersion: majorVersion, + minorVersion: minorVersion + }); + + return { + found: true, + name: name, + version: version, + versionId: versionId, + prettyName: prettyName, + majorVersion: majorVersion, + minorVersion: minorVersion + }; + } + }, + + // Rule: Extract cluster node names from ha.txt (supportconfig) or pacemaker.log/corosync.conf (sosreport) + clusterNodes: { + // Target file path patterns + // supportconfig: */ha.txt + // sosreport: */pacemaker.log or */corosync.conf + // hb_report/crm_report: */corosync.conf or */cib.xml or */crm_mon*.txt + filePattern: /\/(ha\.txt|pacemaker\.log|corosync\.conf|cib\.xml|crm_mon.*\.txt)$/, + + // Parse function receives file content as string + // Returns array of cluster node names and node-to-IP mapping + parse: function(content) { + const lines = content.split('\n'); + const nodeSet = new Set(); + const nodeToIpMap = {}; // Maps hostname to IP from corosync.conf + + // Track if we're inside a nodelist block + let inNodelist = false; + let inNode = false; + let depth = 0; + let currentNodeName = null; + let currentNodeRing0 = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + + // Track nodelist block in corosync.conf + if (trimmed.match(/^nodelist\s*\{/i)) { + inNodelist = true; + depth = 1; + debugLog('[clusterNodes parser] Entered nodelist block at line', i); + continue; + } + + // Inside nodelist, track node blocks + if (inNodelist) { + // Entering a node block + if (trimmed.match(/^node\s*\{/i)) { + inNode = true; + currentNodeName = null; + currentNodeRing0 = null; + debugLog('[clusterNodes parser] Entered node block at line', i); + depth++; + continue; + } + + // Inside a node block, look for name or ring0_addr + if (inNode) { + debugLog('[clusterNodes parser] Checking node line:', trimmed, 'depth:', depth); + + const nameMatch = trimmed.match(/^name:\s*([a-zA-Z][a-zA-Z0-9_\-\.]+)/i); + if (nameMatch) { + currentNodeName = nameMatch[1]; + debugLog('[clusterNodes parser] Found node name:', currentNodeName); + } + + // Match ring0_addr - can be hostname or IP + const ring0Match = trimmed.match(/^ring0_addr:\s*(.+)$/i); + if (ring0Match) { + const addr = ring0Match[1].trim(); + // Check if it's an IP address + if (addr.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { + // It's an IP, store it for later lookup in hosts file + currentNodeRing0 = addr; + debugLog('[clusterNodes parser] Found node ring0_addr (IP):', currentNodeRing0); + } else if (addr.match(/^[a-zA-Z][a-zA-Z0-9_\-\.]+$/)) { + // It's a hostname + currentNodeRing0 = addr; + debugLog('[clusterNodes parser] Found node ring0_addr (hostname):', currentNodeRing0); + } + } + + // Exiting node block - check BEFORE decrementing depth + if (trimmed === '}') { + inNode = false; + depth--; + // Store the name-to-IP mapping if we have both + if (currentNodeName && currentNodeRing0 && currentNodeRing0.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { + nodeToIpMap[currentNodeName] = currentNodeRing0; + debugLog('[clusterNodes parser] Mapped hostname to IP:', currentNodeName, '->', currentNodeRing0); + } + // For corosync.conf: always prefer name, and don't add IPs + // IPs will be added only from other sources (cib.xml, crm_mon) where we can get the actual node names + if (currentNodeName) { + nodeSet.add(currentNodeName); + debugLog('[clusterNodes parser] Added node from name:', currentNodeName); + } + // Note: We intentionally don't add ring0_addr (IP) here anymore + // The IP mapping is preserved in nodeToIpMap for validation purposes + continue; + } + } + + // Check for other braces (not node blocks) + if (trimmed === '}') { + depth--; + } else if (trimmed.endsWith('{')) { + depth++; + } + + // Exiting nodelist block + if (depth === 0) { + inNodelist = false; + debugLog('[clusterNodes parser] Exited nodelist block at line', i); + } + continue; + } + + // Pattern 1: "node X" or "node: X" in cluster configuration + const nodeMatch = trimmed.match(/^node[:\s]+([a-zA-Z][a-zA-Z0-9_\-\.]+)/i); + if (nodeMatch) { + nodeSet.add(nodeMatch[1]); + continue; + } + + // Pattern 2: crm_mon style "Node hostname: online" or "* Node hostname (id):" + const crmNodeMatch = trimmed.match(/(?:\*\s+)?Node\s+([a-zA-Z0-9][a-zA-Z0-9_\-\.]+)(?:\s+\(|:)/i); + if (crmNodeMatch) { + nodeSet.add(crmNodeMatch[1]); + continue; + } + + // Pattern 3: CIB XML node format: + // Prioritize uname over id (uname is the actual hostname, id can be numeric) + const xmlNodeMatch = trimmed.match(/]*>/i); + if (xmlNodeMatch) { + const fullMatch = xmlNodeMatch[0]; + // Try to extract uname first + const unameMatch = fullMatch.match(/uname="([^"]+)"/); + if (unameMatch) { + nodeSet.add(unameMatch[1]); + debugLog('[clusterNodes parser] Found node from XML uname:', unameMatch[1]); + continue; + } + // Fallback to id if uname not found (but skip numeric-only ids) + const idMatch = fullMatch.match(/id="([^"]+)"/); + if (idMatch && !idMatch[1].match(/^\d+$/)) { + nodeSet.add(idMatch[1]); + debugLog('[clusterNodes parser] Found node from XML id:', idMatch[1]); + continue; + } + } + + // Pattern 4: Simple "name: hostname" (not inside nodelist) + if (trimmed.match(/^\s*name:/i)) { + const nameMatch = trimmed.match(/name:\s*([a-zA-Z0-9][a-zA-Z0-9_\-\.]+)/i); + if (nameMatch && !nameMatch[1].match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)) { + nodeSet.add(nameMatch[1]); + } + } + } + + // Filter out common false positives + const excluded = [ + 'online', 'offline', 'standby', 'maintenance', 'pending', 'unclean', + 'node', 'nodes', 'cluster', 'member', 'members', 'list', 'attributes', + 'stonith', 'fencing', 'resource', 'resources', 'status' + ]; + + const filteredNodes = Array.from(nodeSet).filter(n => { + // Allow IP addresses + if (n.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return true; + // Must start with letter for hostnames + if (!n.match(/^[a-zA-Z]/)) return false; + // Must be at least 3 characters + if (n.length < 3) return false; + // Exclude keywords + if (excluded.includes(n.toLowerCase())) return false; + // Valid hostname characters only + if (!n.match(/^[a-zA-Z0-9_\-\.]+$/)) return false; + return true; + }).sort(); + + debugLog('[clusterNodes parser] Found', filteredNodes.length, 'nodes'); + debugLog('[clusterNodes parser] Node-to-IP mappings:', nodeToIpMap); + + return { + nodes: filteredNodes, + nodeToIpMap: nodeToIpMap + }; + } + }, + + // Rule: Extract hosts file from network.txt (supportconfig) or etc/hosts (sosreport/crm_report/hb_report) + hostsFile: { + // Target file path patterns + // supportconfig: */network.txt (contains embedded /etc/hosts) + // sosreport/crm_report/hb_report: */etc/hosts or *etc_hosts + // hb_report may have: */hostname/hosts or */hostname.hosts + filePattern: /\/(network\.txt|etc\/hosts|etc_hosts)$|\/[^\/]+\/(hosts)$/, + + // Parse function receives file content as string + // Handles two formats: + // 1. supportconfig: network.txt with embedded /etc/hosts section + // 2. sosreport: direct /etc/hosts file + // Returns object with hosts entries and validation info + parse: function(content, filename) { + const lines = content.split('\n'); + const hosts = []; + const hostnames = new Set(); + + // Check if this is a direct /etc/hosts file (sosreport) or network.txt (supportconfig) + const isDirectHostsFile = filename && filename.includes('/etc/hosts'); + + let inHostsSection = isDirectHostsFile; // If direct hosts file, we're already in the section + + for (const line of lines) { + // For supportconfig network.txt, detect /etc/hosts section boundaries + if (!isDirectHostsFile) { + // If we're in the hosts section, check for end marker + if (inHostsSection && line.trim().startsWith('#==[ Configuration File ]===')) { + debugLog('[hostsFile parser] Found end marker, stopping'); + break; + } + + // Detect start of /etc/hosts section + if (!inHostsSection) { + if (line.includes('# /etc/hosts')) { + inHostsSection = true; + debugLog('[hostsFile parser] Found /etc/hosts section start'); + continue; + } + continue; // Skip lines until we find the start + } + } + + // Now we're in the hosts section (or processing direct hosts file) + const trimmed = line.trim(); + + // Skip empty lines and comment lines + if (!trimmed || trimmed.startsWith('#')) continue; + + // Parse host entry: IP followed by one or more hostnames + // Format: 192.168.1.10 hostname1 hostname2 hostname3 + const parts = trimmed.split(/\s+/); + if (parts.length < 2) continue; + + const ip = parts[0]; + const names = parts.slice(1); + + // Store entry + hosts.push({ + ip: ip, + hostnames: names + }); + + // Track all hostnames + names.forEach(name => hostnames.add(name)); + } + + debugLog('[hostsFile parser] Extracted', hosts.length, 'host entries with', hostnames.size, 'unique hostnames'); + + return { + entries: hosts, + allHostnames: Array.from(hostnames).sort() + }; + } + }, + + // Rule: Extract and validate corosync.conf from ha.txt (supportconfig) or corosync.conf (sosreport) + corosyncConfig: { + // Target file path patterns + // supportconfig: */ha.txt (contains embedded corosync.conf) + // sosreport: */etc/corosync/corosync.conf + filePattern: /\/(ha\.txt|corosync\.conf)$/, + + // Parse function receives file content as string + // Handles two formats: + // 1. supportconfig: ha.txt with embedded corosync.conf section + // 2. sosreport: direct corosync.conf file + // Extracts corosync.conf section and validates totem token parameter + // Returns object with configuration and validation warnings + parse: function(content, filename) { + const lines = content.split('\n'); + const warnings = []; + let corosyncConf = null; + let totemToken = null; + let totemRetransmits = null; + let totemJoin = null; + let totemConsensus = null; + let totemMaxMessages = null; + let totemTransport = null; + let quorumProvider = null; + let quorumExpectedVotes = null; + let quorumTwoNode = null; + + debugLog('[corosyncConfig parser] Analyzing for corosync.conf'); + + // Check if this is a direct corosync.conf file (sosreport) or ha.txt (supportconfig) + const isDirectCorosyncFile = filename && filename.includes('corosync.conf'); + + // Find the corosync.conf section + let inCorosyncSection = isDirectCorosyncFile; // If direct file, we're already in the section + let corosyncLines = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // For supportconfig ha.txt, detect section boundaries + if (!isDirectCorosyncFile) { + // Check for end marker if we're in the section + if (inCorosyncSection && line.trim().startsWith('#==[ Configuration File ]===')) { + debugLog('[corosyncConfig parser] Found end marker at line', i + 1); + break; + } + + // Detect start of corosync.conf section + if (!inCorosyncSection) { + if (line.includes('# /etc/corosync/corosync.conf')) { + inCorosyncSection = true; + debugLog('[corosyncConfig parser] Found corosync.conf section at line', i + 1); + continue; + } + continue; + } + } + + // We're in the corosync.conf section (or processing direct file) + corosyncLines.push(line); + } + + if (corosyncLines.length === 0) { + debugLog('[corosyncConfig parser] No corosync.conf found in ha.txt'); + return { + found: false, + warnings: [] + }; + } + + debugLog('[corosyncConfig parser] Extracted', corosyncLines.length, 'lines from corosync.conf'); + corosyncConf = corosyncLines.join('\n'); + + // Parse both totem and quorum blocks in a single pass + let inTotemBlock = false; + let inQuorumBlock = false; + let totemBraceDepth = 0; + let quorumBraceDepth = 0; + + for (let i = 0; i < corosyncLines.length; i++) { + const line = corosyncLines[i].trim(); + + // Detect totem block start + if (!inTotemBlock && line.startsWith('totem')) { + inTotemBlock = true; + debugLog('[corosyncConfig parser] Found totem block at line', i + 1); + } + + // Detect quorum block start + if (!inQuorumBlock && line.startsWith('quorum')) { + inQuorumBlock = true; + debugLog('[corosyncConfig parser] *** Found quorum block at line', i + 1, 'line content:', line); + } + + // Parse totem block + if (inTotemBlock) { + // Track brace depth + totemBraceDepth += (line.match(/{/g) || []).length; + totemBraceDepth -= (line.match(/}/g) || []).length; + + // Look for token parameter + const tokenMatch = line.match(/^\s*token\s*:\s*(\d+)/); + if (tokenMatch) { + totemToken = parseInt(tokenMatch[1], 10); + debugLog('[corosyncConfig parser] Found token value:', totemToken); + } + + // Look for token_retransmits_before_loss_const parameter + const retransmitsMatch = line.match(/^\s*token_retransmits_before_loss_const\s*:\s*(\d+)/); + if (retransmitsMatch) { + totemRetransmits = parseInt(retransmitsMatch[1], 10); + debugLog('[corosyncConfig parser] Found token_retransmits_before_loss_const value:', totemRetransmits); + } + + // Look for join parameter + const joinMatch = line.match(/^\s*join\s*:\s*(\d+)/); + if (joinMatch) { + totemJoin = parseInt(joinMatch[1], 10); + debugLog('[corosyncConfig parser] Found join value:', totemJoin); + } + + // Look for consensus parameter + const consensusMatch = line.match(/^\s*consensus\s*:\s*(\d+)/); + if (consensusMatch) { + totemConsensus = parseInt(consensusMatch[1], 10); + debugLog('[corosyncConfig parser] Found consensus value:', totemConsensus); + } + + // Look for max_messages parameter + const maxMessagesMatch = line.match(/^\s*max_messages\s*:\s*(\d+)/); + if (maxMessagesMatch) { + totemMaxMessages = parseInt(maxMessagesMatch[1], 10); + debugLog('[corosyncConfig parser] Found max_messages value:', totemMaxMessages); + } + + // Look for transport parameter (string value) + const transportMatch = line.match(/^\s*transport\s*:\s*(\w+)/); + if (transportMatch) { + totemTransport = transportMatch[1]; + debugLog('[corosyncConfig parser] Found transport value:', totemTransport); + } + + // Exit totem block when braces balance + if (totemBraceDepth === 0 && line.includes('}')) { + inTotemBlock = false; + debugLog('[corosyncConfig parser] Exited totem block at line', i + 1); + } + } + + // Parse quorum block + if (inQuorumBlock) { + // Track brace depth + quorumBraceDepth += (line.match(/{/g) || []).length; + quorumBraceDepth -= (line.match(/}/g) || []).length; + + debugLog('[corosyncConfig parser] Quorum line', i + 1, ':', line, 'depth:', quorumBraceDepth); + + // Look for provider parameter + const providerMatch = line.match(/^\s*provider\s*:\s*(\w+)/); + if (providerMatch) { + quorumProvider = providerMatch[1]; + debugLog('[corosyncConfig parser] Found provider value:', quorumProvider); + } + + // Look for expected_votes parameter + const expectedVotesMatch = line.match(/^\s*expected_votes\s*:\s*(\d+)/); + if (expectedVotesMatch) { + quorumExpectedVotes = parseInt(expectedVotesMatch[1], 10); + debugLog('[corosyncConfig parser] Found expected_votes value:', quorumExpectedVotes); + } + + // Look for two_node parameter + const twoNodeMatch = line.match(/^\s*two_node\s*:\s*(\d+)/); + if (twoNodeMatch) { + quorumTwoNode = parseInt(twoNodeMatch[1], 10); + debugLog('[corosyncConfig parser] Found two_node value:', quorumTwoNode); + } + + // Exit quorum block when braces balance + if (quorumBraceDepth === 0 && line.includes('}')) { + inQuorumBlock = false; + debugLog('[corosyncConfig parser] Exited quorum block at line', i + 1); + } + } + } + + // Log summary of parsed values + debugLog('[corosyncConfig parser] === PARSING SUMMARY ==='); + debugLog('[corosyncConfig parser] Totem values:', { + token: totemToken, + retransmits: totemRetransmits, + join: totemJoin, + consensus: totemConsensus, + maxMessages: totemMaxMessages, + transport: totemTransport + }); + debugLog('[corosyncConfig parser] Quorum values:', { + provider: quorumProvider, + expectedVotes: quorumExpectedVotes, + twoNode: quorumTwoNode + }); + + // Validate token parameter + if (totemToken !== null) { + if (totemToken !== 30000) { + warnings.push({ + parameter: 'totem.token', + expected: 30000, + actual: totemToken, + severity: 'warning', + message: `Totem token value is ${totemToken}, but should be 30000 for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[corosyncConfig parser] WARNING: token value is', totemToken, 'instead of 30000'); + } + } else { + debugLog('[corosyncConfig parser] No token value found in totem block'); + } + + // Validate token_retransmits_before_loss_const value + if (totemRetransmits !== null) { + if (totemRetransmits !== 10) { + warnings.push({ + parameter: 'totem.token_retransmits_before_loss_const', + expected: 10, + actual: totemRetransmits, + severity: 'warning', + message: `Totem token_retransmits_before_loss_const value is ${totemRetransmits}, but should be 10 for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[corosyncConfig parser] WARNING: token_retransmits_before_loss_const value is', totemRetransmits, 'instead of 10'); + } + } else { + debugLog('[corosyncConfig parser] No token_retransmits_before_loss_const value found in totem block'); + } + + // Validate join value + if (totemJoin !== null) { + if (totemJoin !== 60) { + warnings.push({ + parameter: 'totem.join', + expected: 60, + actual: totemJoin, + severity: 'warning', + message: `Totem join value is ${totemJoin}, but should be 60 for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[corosyncConfig parser] WARNING: join value is', totemJoin, 'instead of 60'); + } + } else { + debugLog('[corosyncConfig parser] No join value found in totem block'); + } + + // Validate consensus value + if (totemConsensus !== null) { + if (totemConsensus !== 36000) { + warnings.push({ + parameter: 'totem.consensus', + expected: 36000, + actual: totemConsensus, + severity: 'warning', + message: `Totem consensus value is ${totemConsensus}, but should be 36000 for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[corosyncConfig parser] WARNING: consensus value is', totemConsensus, 'instead of 36000'); + } + } else { + debugLog('[corosyncConfig parser] No consensus value found in totem block'); + } + + // Validate max_messages value + if (totemMaxMessages !== null) { + if (totemMaxMessages !== 20) { + warnings.push({ + parameter: 'totem.max_messages', + expected: 20, + actual: totemMaxMessages, + severity: 'warning', + message: `Totem max_messages value is ${totemMaxMessages}, but should be 20 for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[corosyncConfig parser] WARNING: max_messages value is', totemMaxMessages, 'instead of 20'); + } + } else { + debugLog('[corosyncConfig parser] No max_messages value found in totem block'); + } + + // Validate transport value + if (totemTransport !== null) { + if (totemTransport !== 'udpu') { + warnings.push({ + parameter: 'totem.transport', + expected: 'udpu', + actual: totemTransport, + severity: 'warning', + message: `Totem transport value is '${totemTransport}', but should be 'udpu' for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[corosyncConfig parser] WARNING: transport value is', totemTransport, 'instead of udpu'); + } + } else { + debugLog('[corosyncConfig parser] No transport value found in totem block'); + } + + // Validate quorum provider value + if (quorumProvider !== null) { + if (quorumProvider !== 'corosync_votequorum') { + warnings.push({ + parameter: 'quorum.provider', + expected: 'corosync_votequorum', + actual: quorumProvider, + severity: 'warning', + message: `Quorum provider value is '${quorumProvider}', but should be 'corosync_votequorum' for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[corosyncConfig parser] WARNING: provider value is', quorumProvider, 'instead of corosync_votequorum'); + } + } else { + debugLog('[corosyncConfig parser] No provider value found in quorum block'); + } + + // Validate quorum expected_votes value + if (quorumExpectedVotes !== null) { + if (quorumExpectedVotes !== 2) { + warnings.push({ + parameter: 'quorum.expected_votes', + expected: 2, + actual: quorumExpectedVotes, + severity: 'warning', + message: `Quorum expected_votes value is ${quorumExpectedVotes}, but should be 2 for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[corosyncConfig parser] WARNING: expected_votes value is', quorumExpectedVotes, 'instead of 2'); + } + } else { + debugLog('[corosyncConfig parser] No expected_votes value found in quorum block'); + } + + // Validate quorum two_node value + if (quorumTwoNode !== null) { + if (quorumTwoNode !== 1) { + warnings.push({ + parameter: 'quorum.two_node', + expected: 1, + actual: quorumTwoNode, + severity: 'warning', + message: `Quorum two_node value is ${quorumTwoNode}, but should be 1 for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[corosyncConfig parser] WARNING: two_node value is', quorumTwoNode, 'instead of 1'); + } + } else { + debugLog('[corosyncConfig parser] No two_node value found in quorum block'); + } + + return { + found: true, + totemToken: totemToken, + totemRetransmits: totemRetransmits, + totemJoin: totemJoin, + totemConsensus: totemConsensus, + totemMaxMessages: totemMaxMessages, + totemTransport: totemTransport, + quorumProvider: quorumProvider, + quorumExpectedVotes: quorumExpectedVotes, + quorumTwoNode: quorumTwoNode, + warnings: warnings, + configLength: corosyncLines.length + }; + } + }, + + // Rule: Detect Pacemaker cluster resources + pacemakerResources: { + // Target file patterns - pacemaker CIB (Cluster Information Base) or crm config + // supportconfig: */ha.txt (contains embedded crm_mon or cib.xml sections) + // sosreport/crm_report: */cib.xml, */crm_mon*.txt, */crm*config, */pacemaker.log + filePattern: /cib\.xml$|\/crm_mon.*\.txt$|\/crm.*config$|pacemaker\.log$|\/ha\.txt$/, + + parse: function(content, filename) { + debugLog('[pacemakerResources parser] Analyzing pacemaker configuration in:', filename); + + const resources = []; + const lines = content.split('\n'); + + // Pattern 1: CIB XML format - + // Pattern 2: crm config format - primitive resource-name type:provider + // Pattern 3: crm_mon output format - resource-name (type::provider) + + // For ha.txt files, we need to extract relevant sections to avoid false matches + let relevantContent = content; + const isHaTxt = filename.includes('ha.txt'); + let crmMonSections = []; // Declare outside the if block so it's accessible in debug code + + if (isHaTxt) { + // For ha.txt, we want to extract BOTH CIB XML and crm_mon sections + // First pass: get resources from CIB XML + // Second pass: enrich with node info from crm_mon + const cibStart = content.indexOf(''); + + // Look for crm_mon sections - we want ALL of them, not just one + // Different flags show different info: + // -r shows resources + // -n shows node names + // -A shows attributes + // Split content by command section markers + const sectionParts = content.split(/^#==\[ Command \]====/m); + + let crmStatusSection = null; + + debugLog('[pacemakerResources parser] Split into', sectionParts.length, 'parts'); + + for (let i = 1; i < sectionParts.length; i++) { // Skip first part (before first marker) + const part = sectionParts[i]; + + // Debug: show first 200 chars of each part + if (i <= 3) { + debugLog(`[pacemakerResources parser] Part ${i} preview:`, part.substring(0, 200).replace(/\n/g, '\\n')); + } + + // Extract command line + // After split, each part starts with the rest of the marker line (====#) + // Then newline, then # command + // Pattern: skip any ='s and whitespace, find line starting with # + const commandLineMatch = part.match(/^[=\s]*#\s*([^\n]+)/); + if (!commandLineMatch) { + debugLog(`[pacemakerResources parser] Part ${i}: No command line match found`); + debugLog(`[pacemakerResources parser] Part ${i} first 100 chars:`, JSON.stringify(part.substring(0, 100))); + continue; + } const commandLine = commandLineMatch[1].trim(); + // Extract content (everything after the command line) + const contentStart = part.indexOf('\n', commandLineMatch[0].length); + const sectionContent = contentStart >= 0 ? part.substring(contentStart + 1) : ''; + + debugLog('[pacemakerResources parser] Found command section:', commandLine.substring(0, 100)); + + // Check if this is crm_mon command (not rpm -qa with crm packages) + if (commandLine.includes('crm_mon') && !commandLine.includes('rpm') && !commandLine.includes('egrep')) { + crmMonSections.push(sectionContent); + debugLog('[pacemakerResources parser] Matched crm_mon section:', commandLine); + debugLog('[pacemakerResources parser] Section content length:', sectionContent.length, 'chars'); + debugLog('[pacemakerResources parser] First 500 chars:', sectionContent.substring(0, 500)); + } else if (commandLine.match(/^crm\s+status/) && !commandLine.includes('rpm')) { + crmStatusSection = sectionContent; + debugLog('[pacemakerResources parser] Matched crm status section'); + } + } + + // Combine all relevant sections + let sections = []; + + if (cibStart !== -1 && cibEnd !== -1) { + sections.push(content.substring(cibStart, cibEnd + 6)); + debugLog('[pacemakerResources parser] Found CIB XML section in ha.txt'); + } + + if (crmMonSections.length > 0) { + // Add ALL crm_mon sections - each might have different information + sections.push(...crmMonSections); + debugLog('[pacemakerResources parser] Found', crmMonSections.length, 'crm_mon section(s) in ha.txt'); + debugLog('[pacemakerResources parser] First crm_mon section preview (first 500 chars):', crmMonSections[0].substring(0, 500)); + } + + if (crmStatusSection) { + sections.push(crmStatusSection); + debugLog('[pacemakerResources parser] Found crm status section in ha.txt'); + } + + // If we found multiple sections, combine them + if (sections.length > 0) { + relevantContent = sections.join('\n\n'); + debugLog('[pacemakerResources parser] Combined', sections.length, 'sections from ha.txt'); + } else { + // Try crm configure show as last resort + const crmConfigSection = content.match(/^#==\[ Command \]====.*?crm configure show.*?(?=^#==\[|$)/ms); + if (crmConfigSection) { + relevantContent = crmConfigSection[0]; + debugLog('[pacemakerResources parser] Found crm configure section in ha.txt'); + } else { + debugLog('[pacemakerResources parser] No recognized cluster sections found in ha.txt'); + return { found: false }; + } + } + } + + const contentLines = relevantContent.split('\n'); + + debugLog('[pacemakerResources parser] Processing', contentLines.length, 'lines from', isHaTxt ? 'ha.txt sections' : filename); + + // Debug: Show actual content of each crm_mon section + if (isHaTxt && crmMonSections.length > 0) { + debugLog('[pacemakerResources parser] Showing content of', crmMonSections.length, 'crm_mon sections:'); + crmMonSections.forEach((section, idx) => { + const lines = section.split('\n').filter(l => l.trim() && !l.includes('#==[ Command ]====')); + debugLog(` crm_mon section ${idx + 1}: ${lines.length} non-empty lines`); + lines.slice(0, 30).forEach((l, i) => debugLog(` [${i}]:`, l.substring(0, 200))); + }); + + // Look for lines that match crm_mon output format (not XML) + const crmMonLines = contentLines.filter(l => { + const trimmed = l.trim(); + // Look for lines with * or indentation followed by resource name and parentheses + return (trimmed.startsWith('*') || /^\s+\*/.test(l)) && + (trimmed.includes('(ocf::') || trimmed.includes('(stonith:')); + }); + + debugLog('[pacemakerResources parser] Lines matching crm_mon format:', crmMonLines.length, 'found'); + crmMonLines.slice(0, 20).forEach((l, i) => debugLog(` crm_mon[${i}]:`, l)); + } + + for (const line of contentLines) { + const trimmed = line.trim(); + + // XML format: + const xmlMatch = trimmed.match(/ r.name === name); + if (existing) { + // Enrich existing resource with node info + existing.node = node || null; + existing.status = status; + debugLog('[pacemakerResources parser] Enriched resource with node info:', name, 'on', node || 'unknown', 'status:', status); + } else { + // Add new resource + resources.push({ + name: name, + type: type, + provider: provider, + class: cls, + format: 'crm_mon', + node: node || null, + status: status + }); + debugLog('[pacemakerResources parser] Found resource (crm_mon):', name, type, 'on', node || 'unknown', 'status:', status); + } + continue; + } + + // Pattern for lines without parentheses but with colon separator + // Example: * stonith-sbd (stonith:external/sbd): Started cceccerprd02 + // Format: name (type:provider/agent): Status node + const stonithMatch = trimmed.match(/^\*?\s*(\S+)\s+\((\S+):(\S+)\/(\S+)\):\s+(\w+)(?:\s+(\S+))?/); + if (stonithMatch) { + const [, name, type, provider, agent, status, node] = stonithMatch; + + const existing = resources.find(r => r.name === name); + if (existing) { + existing.node = node || null; + existing.status = status; + debugLog('[pacemakerResources parser] Enriched resource (stonith):', name, 'on', node || 'unknown'); + } else { + resources.push({ + name: name, + type: agent, + provider: provider, + class: type, + format: 'crm_mon_stonith', + node: node || null, + status: status + }); + debugLog('[pacemakerResources parser] Found resource (stonith format):', name, agent, 'on', node || 'unknown'); + } + continue; + } + + // Additional pattern for "crm status" or alternate crm_mon format + // Full line: resource-name (class:provider:type): Status node-name + const statusMatch = trimmed.match(/^(\S+)\s+\((\S+):(\S+):(\S+)\):\s+(\w+)\s+(\S+)/); + if (statusMatch) { + const [, name, cls, provider, type, status, node] = statusMatch; + + const existing = resources.find(r => r.name === name); + if (existing) { + existing.node = node || null; + existing.status = status; + debugLog('[pacemakerResources parser] Enriched resource (alt format):', name, 'on', node); + } else { + resources.push({ + name: name, + type: type, + provider: provider, + class: cls, + format: 'status', + node: node || null, + status: status + }); + debugLog('[pacemakerResources parser] Found resource (status format):', name, type, 'on', node); + } + continue; + } + } + + if (resources.length === 0) { + debugLog('[pacemakerResources parser] No resources found'); + return { found: false }; + } + + // Second pass: Extract node information from XML rsc_location constraints + // These show where resources are configured to run + // Format: + debugLog('[pacemakerResources parser] Looking for rsc_location constraints in XML...'); + for (const line of contentLines) { + const trimmed = line.trim(); + const locMatch = trimmed.match(/ r.name === rscName); + if (resource && !resource.node) { + resource.node = node; + resource.status = role || 'Started'; + debugLog('[pacemakerResources parser] Enriched from XML constraint:', rscName, 'on', node, 'role:', role || 'Started'); + } + } + } + + debugLog('[pacemakerResources parser] Found', resources.length, 'resources'); + debugLog('[pacemakerResources parser] Resources with node info:', + resources.filter(r => r.node).map(r => `${r.name} on ${r.node} (${r.status || 'unknown'})`)); + + // Parse Failed Resource Actions section + const failedActions = []; + let inFailedSection = false; + for (const line of contentLines) { + const trimmed = line.trim(); + + // Detect section start + if (trimmed === 'Failed Resource Actions:' || trimmed === 'Failed Actions:') { + inFailedSection = true; + debugLog('[pacemakerResources parser] Found Failed Resource Actions section'); + continue; + } + + // Detect section end (empty line or next section) + if (inFailedSection && (trimmed === '' || trimmed.match(/^[A-Z]/))) { + if (trimmed.match(/^(Node Attributes|Migration Summary|Tickets):/)) { + inFailedSection = false; + } + continue; + } + + // Parse failed action lines + // Format: * resource_name_monitor_10000 on node 'not running' (7): call=123, status=complete, exitreason='...', last-rc-change='timestamp', queued=0ms, exec=0ms + if (inFailedSection && trimmed.startsWith('*')) { + const failMatch = trimmed.match(/^\*\s+(\S+)\s+on\s+(\S+)\s+'([^']+)'\s+\((\d+)\):\s+(.+)/); + if (failMatch) { + const [, action, node, state, code, details] = failMatch; + failedActions.push({ + action: action, + node: node, + state: state, + returnCode: code, + details: details + }); + debugLog('[pacemakerResources parser] Found failed action:', action, 'on', node); + } + } + } + + debugLog('[pacemakerResources parser] Found', failedActions.length, 'failed actions'); + + return { + found: true, + resources: resources, + count: resources.length, + failedActions: failedActions, + failedActionsCount: failedActions.length + }; + } + }, + + // Rule: Detect corosync runtime status from corosync-cfgtool + corosyncStatus: { + filePattern: /corosync-cfgtool.*-s$/, + + parse: function(content, filename) { + debugLog('[corosyncStatus parser] Analyzing corosync runtime status in:', filename); + + const lines = content.split('\n'); + let isValid = true; + let errorMessage = null; + let localNodeId = null; + let transport = null; + const nodes = []; + + for (const line of lines) { + const trimmed = line.trim(); + + // Check for configuration error + if (trimmed.match(/Could not initialize corosync configuration/i)) { + isValid = false; + errorMessage = trimmed; + debugLog('[corosyncStatus parser] Corosync configuration error detected:', trimmed); + break; + } + + // Parse local node ID and transport + const localNodeMatch = trimmed.match(/Local node ID\s+(\d+).*transport\s+(\w+)/i); + if (localNodeMatch) { + localNodeId = localNodeMatch[1]; + transport = localNodeMatch[2]; + debugLog('[corosyncStatus parser] Local node ID:', localNodeId, 'Transport:', transport); + } + + // Parse node status lines (format: "nodeid: 1: localhost" or "nodeid: 2: connected") + const nodeMatch = trimmed.match(/nodeid:\s*(\d+):\s*(\w+)/i); + if (nodeMatch) { + const nodeId = nodeMatch[1]; + const status = nodeMatch[2]; + nodes.push({ nodeId: nodeId, status: status }); + debugLog('[corosyncStatus parser] Node', nodeId, 'status:', status); + } + } + + if (!isValid) { + return { + found: true, + valid: false, + error: errorMessage || 'Could not initialize corosync configuration' + }; + } + + return { + found: true, + valid: true, + localNodeId: localNodeId, + transport: transport, + nodes: nodes + }; + } + }, + + // Rule: Detect fencing/STONITH configuration + fencingConfig: { + // Target file patterns - match cib.xml anywhere in the archive + filePattern: /cib\.xml$|\/crm_mon.*\.txt$|\/crm.*config$|stonith|\/ha\.txt$/, + + parse: function(content, filename) { + debugLog('[fencingConfig parser] Analyzing fencing configuration in:', filename); + + const fencingDevices = []; + const lines = content.split('\n'); + let stonithEnabled = null; + let stonithSourceFile = null; + let stonithSourcePattern = null; + + for (let lineNum = 0; lineNum < lines.length; lineNum++) { + const line = lines[lineNum]; + const trimmed = line.trim(); + + // Check if STONITH is enabled + // Format: stonith-enabled=true or + if (trimmed.match(/stonith-enabled[=\s]*true/i)) { + stonithEnabled = true; + stonithSourceFile = filename; + stonithSourcePattern = 'stonith-enabled=true'; + debugLog('[fencingConfig parser] STONITH is enabled at line', lineNum + 1); + } else if (trimmed.match(/name="stonith-enabled".*value="true"/i)) { + stonithEnabled = true; + stonithSourceFile = filename; + stonithSourcePattern = ''; + debugLog('[fencingConfig parser] STONITH is enabled (XML) at line', lineNum + 1); + } + + if (trimmed.match(/stonith-enabled[=\s]*false/i)) { + stonithEnabled = false; + stonithSourceFile = filename; + stonithSourcePattern = 'stonith-enabled=false'; + debugLog('[fencingConfig parser] STONITH is disabled at line', lineNum + 1); + } else if (trimmed.match(/name="stonith-enabled".*value="false"/i)) { + stonithEnabled = false; + stonithSourceFile = filename; + stonithSourcePattern = ''; + debugLog('[fencingConfig parser] STONITH is disabled (XML) at line', lineNum + 1); + } + + // Detect Azure fencing agent: fence_azure_arm + // XML: + // crm: primitive stonith-fence_azure_arm stonith:fence_azure_arm + const azureFenceMatch = trimmed.match(/(?:primitive.*?id="|primitive\s+)([^"\s]+).*?fence_azure_arm/); + if (azureFenceMatch) { + const deviceName = azureFenceMatch[1]; + if (!fencingDevices.find(d => d.name === deviceName)) { + const isXml = trimmed.includes('' : 'crm: primitive ... fence_azure_arm' + }); + debugLog('[fencingConfig parser] Found Azure fencing agent:', deviceName, 'at line', lineNum + 1); + } + } + + // Detect other common fencing agents + const fenceMatch = trimmed.match(/(?:primitive.*?id="|primitive\s+)([^"\s]+).*?stonith:(\S+)/); + if (fenceMatch) { + const [, deviceName, agentType] = fenceMatch; + if (!fencingDevices.find(d => d.name === deviceName)) { + let agent = agentType; + let cloud = null; + + // Identify cloud-specific agents + if (agentType.includes('azure')) { + agent = 'Azure Fencing'; + cloud = 'Azure'; + } else if (agentType.includes('aws')) { + agent = 'AWS Fencing'; + cloud = 'AWS'; + } else if (agentType.includes('gce')) { + agent = 'GCP Fencing'; + cloud = 'GCP'; + } + + const isXml = trimmed.includes('' : `crm: primitive ... stonith:${agentType}` + }); + debugLog('[fencingConfig parser] Found fencing device:', deviceName, agentType, 'at line', lineNum + 1); + } + } + + // Also check XML format: or type="external/XXX"> + const xmlFenceMatch = trimmed.match(/ d.name === deviceName)) { + let agent = agentType; + let cloud = null; + + // Handle external/sbd format + if (agentType.startsWith('external/')) { + const externalType = agentType.split('/')[1]; + agent = `External ${externalType.toUpperCase()}`; + } else if (agentType.includes('azure') || agentType.includes('fence_azure_arm')) { + agent = 'Azure Fencing'; + cloud = 'Azure'; + } else if (agentType.includes('aws')) { + agent = 'AWS Fencing'; + cloud = 'AWS'; + } else if (agentType.includes('gce')) { + agent = 'GCP Fencing'; + cloud = 'GCP'; + } else if (agentType.startsWith('fence_')) { + // Generic fence agent + const fenceType = agentType.replace('fence_', ''); + agent = `Fence ${fenceType.toUpperCase()}`; + } + + fencingDevices.push({ + name: deviceName, + type: agentType, + agent: agent, + cloud: cloud, + sourceFile: filename, + sourceLine: lineNum + 1, + pattern: `XML: ` + }); + debugLog('[fencingConfig parser] Found fencing device (XML):', deviceName, agentType, 'at line', lineNum + 1); + } + } + } + + // If we found fencing devices but stonith-enabled wasn't explicitly set, infer it's enabled + if (fencingDevices.length > 0 && stonithEnabled === null) { + stonithEnabled = true; + stonithSourceFile = filename; + stonithSourcePattern = 'Inferred from presence of stonith devices'; + debugLog('[fencingConfig parser] STONITH status inferred as enabled (found', fencingDevices.length, 'devices)'); + } + + debugLog('[fencingConfig parser] STONITH enabled:', stonithEnabled); + debugLog('[fencingConfig parser] Found', fencingDevices.length, 'fencing devices'); + + return { + found: true, + stonithEnabled: stonithEnabled, + stonithSourceFile: stonithSourceFile, + stonithSourcePattern: stonithSourcePattern, + fencingDevices: fencingDevices, + count: fencingDevices.length + }; + } + }, + + // Rule: Detect cluster events (resource migrations and fencing events) from pacemaker/cluster logs + clusterEvents: { + // Target file patterns - pacemaker.log, corosync.log, cluster.log, ha-log, messages + // Also includes journalctl output and crm_report archives + filePattern: /\/(pacemaker\.log|corosync\.log|cluster\.log|ha-log|messages|journalctl[^\/]*)(?:[.-]\d+)?(?:\.txt)?$/, + + parse: function(content, filename) { + debugLog('[clusterEvents parser] Analyzing cluster logs in:', filename); + + const resourceMigrations = []; + const fencingEvents = []; + const lines = content.split('\n'); + + for (let lineNum = 0; lineNum < lines.length; lineNum++) { + const line = lines[lineNum]; + const trimmed = line.trim(); + + // Skip empty lines + if (!trimmed) continue; + + // Parse timestamp (various formats supported) + // ISO format: 2025-11-11T10:30:45.123456+00:00 + // Syslog format: Nov 11 10:30:45 + // Journal format: Nov 11 10:30:45.123456 + let timestamp = null; + const isoMatch = trimmed.match(/^(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?)/); + const syslogMatch = trimmed.match(/^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)/); + + if (isoMatch) { + timestamp = isoMatch[1]; + } else if (syslogMatch) { + timestamp = syslogMatch[1]; + } + + // Detect resource migration/move events + // Patterns: + // - "Moving resource from to " + // - "Migrating from to " + // - "notice: pcmk_graph_info: Transition: Starting on " + // - "notice: Operation _start_0: ok (node=)" + // - "Resource is active on (previously on )" + + const moveMatch = trimmed.match(/(?:Moving|Migrating)\s+(?:resource\s+)?(\S+)\s+from\s+(\S+)\s+to\s+(\S+)/i); + if (moveMatch) { + const [, resource, fromNode, toNode] = moveMatch; + resourceMigrations.push({ + timestamp: timestamp, + resource: resource, + fromNode: fromNode, + toNode: toNode, + action: 'migration', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) // Truncate long lines + }); + debugLog('[clusterEvents parser] Found resource migration:', resource, 'from', fromNode, 'to', toNode); + continue; + } + + // Starting resource on node + // Only match Pacemaker cluster resource start events + // Explicitly reject systemd and init script messages + const startMatch = trimmed.match(/(?:Starting|Transition.*Starting)\s+(\S+)\s+on\s+(\S+)/i); + if (startMatch) { + const [, resource, node] = startMatch; + + // Reject systemd messages explicitly + if (trimmed.includes('systemd')) continue; + + // Reject lines with process ID patterns like [123]: + if (/\[\d+\]:/.test(trimmed)) continue; + + // Exclude systemd service type names + const isSystemdService = resource.match(/\.(service|target|socket|mount|swap|path|timer|device|scope|slice)$/i) || + resource.includes('@'); // systemd template units like service@instance + + // Only add if not systemd service and not a duplicate from same line + if (!isSystemdService && + !resourceMigrations.find(m => m.resource === resource && m.toNode === node && m.sourceLine === lineNum + 1)) { + resourceMigrations.push({ + timestamp: timestamp, + resource: resource, + fromNode: null, + toNode: node, + action: 'start', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found resource start:', resource, 'on', node); + } + continue; + } + + // Stopping resource on node + // Only match Pacemaker cluster resource stop events + // Explicitly reject systemd and init script messages + const stopMatch = trimmed.match(/(?:Stopping|Stopped)\s+(\S+)\s+on\s+(\S+)/i); + if (stopMatch) { + const [, resource, node] = stopMatch; + + // Reject systemd messages explicitly + if (trimmed.includes('systemd')) continue; + + // Reject lines with process ID patterns like [123]: + if (/\[\d+\]:/.test(trimmed)) continue; + + // Exclude systemd service type names + const isSystemdService = resource.match(/\.(service|target|socket|mount|swap|path|timer|device|scope|slice)$/i) || + resource.includes('@'); // systemd template units like service@instance + + if (!isSystemdService) { + resourceMigrations.push({ + timestamp: timestamp, + resource: resource, + fromNode: node, + toNode: null, + action: 'stop', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found resource stop:', resource, 'on', node); + } + continue; + } + + // Resource operation format: "Operation resource_start_0: ok (node=nodeX)" + const opMatch = trimmed.match(/Operation\s+(\S+?)_(?:start|stop|monitor|migrate)_\d+:\s*\w+\s*\(node=(\S+)\)/i); + if (opMatch) { + const [, resource, node] = opMatch; + const isStart = trimmed.includes('_start_'); + const isStop = trimmed.includes('_stop_'); + + if (isStart) { + if (!resourceMigrations.find(m => m.resource === resource && m.toNode === node && m.sourceLine === lineNum + 1)) { + resourceMigrations.push({ + timestamp: timestamp, + resource: resource, + fromNode: null, + toNode: node, + action: 'start', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found resource operation start:', resource, 'on', node); + } + } else if (isStop) { + resourceMigrations.push({ + timestamp: timestamp, + resource: resource, + fromNode: node, + toNode: null, + action: 'stop', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found resource operation stop:', resource, 'on', node); + } + continue; + } + + // Detect fencing/STONITH events + // Patterns: + // - "stonith-ng: Succeeded: st_notify" + // - "Fencing : success" + // - "stonith: Succeeded: st_delete_device_0 on " + // - "Requesting fencing ([on|reboot|off]) of node " + // - "fence_azure_arm: Called fence_azure_arm for " + // - "Node will be fenced" + + const fenceRequestMatch = trimmed.match(/Requesting\s+fencing\s+\((\w+)\)\s+(?:of\s+)?(?:node\s+)?(\S+)/i); + if (fenceRequestMatch) { + const [, action, node] = fenceRequestMatch; + fencingEvents.push({ + timestamp: timestamp, + targetNode: node, + action: action, + status: 'requested', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found fencing request:', action, 'of node', node); + continue; + } + + const fenceSuccessMatch = trimmed.match(/(?:Fencing|stonith.*?fence)\s+(\S+).*?(?:success|succeeded)/i); + if (fenceSuccessMatch) { + const node = fenceSuccessMatch[1]; + fencingEvents.push({ + timestamp: timestamp, + targetNode: node, + action: 'fence', + status: 'success', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found successful fencing event:', node); + continue; + } + + // Additional success patterns + // "Operation stonith-X_monitor_0: ok" or "Operation X_reboot_0: ok" + const stonithOpSuccess = trimmed.match(/Operation\s+(?:stonith-)?(\S+?)_(?:reboot|monitor|on|off)_\d+:\s*ok/i); + if (stonithOpSuccess && (trimmed.toLowerCase().includes('stonith') || trimmed.toLowerCase().includes('fence'))) { + const node = stonithOpSuccess[1]; + if (!fencingEvents.find(e => e.targetNode === node && e.sourceLine === lineNum + 1)) { + fencingEvents.push({ + timestamp: timestamp, + targetNode: node, + action: 'fence', + status: 'success', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found successful stonith operation:', node); + } + continue; + } + + // "Peer was terminated (reboot) by on behalf of" + // "stonith_api_time: was fenced (reboot) by " + const peerTerminated = trimmed.match(/(?:Peer|peer)\s+(\S+)\s+was\s+(?:terminated|fenced)\s+\((\w+)\)/i); + if (peerTerminated) { + const [, node, action] = peerTerminated; + if (!fencingEvents.find(e => e.targetNode === node && e.sourceLine === lineNum + 1)) { + fencingEvents.push({ + timestamp: timestamp, + targetNode: node, + action: action, + status: 'success', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found peer termination (success):', node, action); + } + continue; + } + + // "stonith_api_time: was fenced successfully" + const apiSuccess = trimmed.match(/stonith.*?(\S+)\s+was\s+fenced\s+successfully/i); + if (apiSuccess) { + const node = apiSuccess[1]; + if (!fencingEvents.find(e => e.targetNode === node && e.sourceLine === lineNum + 1)) { + fencingEvents.push({ + timestamp: timestamp, + targetNode: node, + action: 'fence', + status: 'success', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found stonith API success:', node); + } + continue; + } + + const fenceFailMatch = trimmed.match(/(?:Fencing|stonith.*?fence)\s+(\S+).*?(?:fail|error)/i); + if (fenceFailMatch) { + const node = fenceFailMatch[1]; + fencingEvents.push({ + timestamp: timestamp, + targetNode: node, + action: 'fence', + status: 'failed', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found failed fencing event:', node); + continue; + } + + // "Node will be fenced" + const fenceWillMatch = trimmed.match(/(?:Node|peer)\s+(\S+)\s+will\s+be\s+fenced/i); + if (fenceWillMatch) { + const node = fenceWillMatch[1]; + if (!fencingEvents.find(e => e.targetNode === node && e.sourceLine === lineNum + 1)) { + fencingEvents.push({ + timestamp: timestamp, + targetNode: node, + action: 'fence', + status: 'pending', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found pending fencing:', node); + } + continue; + } + + // fence_azure_arm or other fence agent calls + const fenceAgentMatch = trimmed.match(/(fence_\w+).*?(?:Called|for)\s+.*?(?:node\s+)?(\S+)/i); + if (fenceAgentMatch && (trimmed.toLowerCase().includes('fence') || trimmed.toLowerCase().includes('stonith'))) { + const [, agent, node] = fenceAgentMatch; + if (!fencingEvents.find(e => e.targetNode === node && e.sourceLine === lineNum + 1)) { + fencingEvents.push({ + timestamp: timestamp, + targetNode: node, + action: 'fence', + agent: agent, + status: 'in_progress', + sourceFile: filename, + sourceLine: lineNum + 1, + logLine: trimmed.substring(0, 200) + }); + debugLog('[clusterEvents parser] Found fence agent call:', agent, 'for node', node); + } + continue; + } + } + + if (resourceMigrations.length === 0 && fencingEvents.length === 0) { + debugLog('[clusterEvents parser] No cluster events found'); + return { found: false }; + } + + debugLog('[clusterEvents parser] Found', resourceMigrations.length, 'resource events and', fencingEvents.length, 'fencing events'); + return { + found: true, + resourceMigrations: resourceMigrations, + fencingEvents: fencingEvents, + totalEvents: resourceMigrations.length + fencingEvents.length + }; + } + }, + + // Rule: Detect Hyper-V Live Migration events from message logs + liveMigration: { + // Target file path patterns + // supportconfig: */messages or */localmessages (with optional suffixes) + // sosreport: */var/log/messages or */sos_commands/logs/journalctl* + // Matches: messages, messages.1, messages-20251007.txt, localmessages, etc. + filePattern: /\/(pacemaker\.log|corosync\.log|cluster\.log|ha-log|messages|localmessages|journalctl[^\/]*)(?:[.-]\d+)?(?:\.txt)?$/, + + // Parse function receives file content as string + // Detects Live Migration events by finding the sequence: + // "hv_utils: Heartbeat IC" -> "hv_balloon" -> "hv_netvsc" within 100 lines + // Returns array of detected migration events with timestamps + parse: function(content) { + const lines = content.split('\n'); + const migrations = []; + + debugLog('[liveMigration parser] Analyzing', lines.length, 'lines'); + + // Debug: Count occurrences of each pattern + let heartbeatCount = 0; + let balloonCount = 0; + let netvscCount = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Count patterns for debugging + if (line.includes('hv_utils: Heartbeat IC')) heartbeatCount++; + if (line.includes('hv_balloon')) balloonCount++; + if (line.includes('hv_netvsc')) netvscCount++; + + // Look for first pattern: "hv_utils: Heartbeat IC" + if (line.includes('hv_utils: Heartbeat IC')) { + const heartbeatLine = i; + const heartbeatTimestamp = this.extractTimestamp(line); + + debugLog('[liveMigration parser] Found hv_utils at line', i + 1, ':', heartbeatTimestamp); + + // Search next 100 lines for "hv_balloon" + let balloonLine = -1; + for (let j = i + 1; j < Math.min(i + 100, lines.length); j++) { + if (lines[j].includes('hv_balloon')) { + balloonLine = j; + debugLog('[liveMigration parser] Found hv_balloon at line', j + 1, '(+' + (j - i) + ' lines)'); + break; + } + } + + // If found hv_balloon, search for "hv_netvsc" + if (balloonLine !== -1) { + let netvscLine = -1; + for (let k = balloonLine + 1; k < Math.min(heartbeatLine + 100, lines.length); k++) { + if (lines[k].includes('hv_netvsc')) { + netvscLine = k; + debugLog('[liveMigration parser] Found hv_netvsc at line', k + 1, '(+' + (k - heartbeatLine) + ' lines from heartbeat)'); + break; + } + } + + // If found all three, we detected a Live Migration + if (netvscLine !== -1) { + // Extract timestamp from the heartbeat line + const timestamp = this.extractTimestamp(lines[heartbeatLine]); + + migrations.push({ + timestamp: timestamp || 'Unknown', + lineNumber: heartbeatLine + 1, + rawLine: lines[heartbeatLine] + }); + + debugLog('[liveMigration parser] ✓ Detected migration at line', heartbeatLine + 1, ':', timestamp); + + // Skip ahead to avoid duplicate detections + i = netvscLine; + } else { + debugLog('[liveMigration parser] ✗ No hv_netvsc found within range'); + } + } else { + debugLog('[liveMigration parser] ✗ No hv_balloon found within 100 lines'); + } + } + } + + debugLog('[liveMigration parser] Pattern summary: heartbeat=' + heartbeatCount + ', balloon=' + balloonCount + ', netvsc=' + netvscCount); + debugLog('[liveMigration parser] Found', migrations.length, 'Live Migration events'); + + return { + count: migrations.length, + events: migrations + }; + }, + + // Helper function to extract timestamp from log line + extractTimestamp: function(line) { + // Common syslog timestamp patterns: + // 1. "2025-10-23T14:30:45.123456+00:00" + // 2. "Oct 23 14:30:45" + // 3. "2025-10-23 14:30:45" + + // Try ISO timestamp + const isoMatch = line.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?)/); + if (isoMatch) return isoMatch[1]; + + // Try syslog format (Month Day Time) + const syslogMatch = line.match(/((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})/); + if (syslogMatch) return syslogMatch[1]; + + // Try simple date format + const simpleMatch = line.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/); + if (simpleMatch) return simpleMatch[1]; + + return null; + } + }, + + // Rule: Detect Linux kernel reboots from message logs + kernelReboots: { + // Target file path patterns (same as liveMigration) + // supportconfig: */messages or */localmessages (with optional suffixes) + // sosreport: */var/log/messages or */sos_commands/logs/journalctl* + filePattern: /\/(messages|localmessages|journalctl[^\/]*)(?:[.-]\d+)?(?:\.txt)?$/, + + // Parse function receives file content as string + // Detects kernel reboots by finding patterns: + // - "Linux version" (kernel boot message) + // - "Command line:" (kernel command line) + // - System restart messages + // Returns array of detected reboot events with timestamps and kernel versions + parse: function(content) { + const lines = content.split('\n'); + const reboots = []; + + debugLog('[kernelReboots parser] Analyzing', lines.length, 'lines for kernel reboots'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Pattern 1: "Linux version X.Y.Z" - primary boot message + const kernelMatch = line.match(/kernel:\s*Linux version\s+([\d\.\-\w]+)/i); + if (kernelMatch) { + const timestamp = this.extractTimestamp(line); + const kernelVersion = kernelMatch[1]; + + reboots.push({ + timestamp: timestamp || 'Unknown', + lineNumber: i + 1, + kernelVersion: kernelVersion, + type: 'kernel_boot', + rawLine: line.trim() + }); + + debugLog('[kernelReboots parser] ✓ Detected kernel boot at line', i + 1, ':', timestamp, 'version:', kernelVersion); + continue; + } + + // Pattern 2: systemd reboot messages + if (line.match(/systemd.*Shutting down/i) || + line.match(/systemd.*Starting Reboot/i) || + line.match(/systemd.*Stopped target.*Shutdown/i)) { + const timestamp = this.extractTimestamp(line); + + // Check if we already have a reboot event very close to this timestamp + const isDuplicate = reboots.some(r => { + if (!timestamp || !r.timestamp) return false; + // Simple duplicate check - same line or very close + return Math.abs(r.lineNumber - (i + 1)) < 5; + }); + + if (!isDuplicate) { + reboots.push({ + timestamp: timestamp || 'Unknown', + lineNumber: i + 1, + kernelVersion: null, + type: 'systemd_shutdown', + rawLine: line.trim() + }); + + debugLog('[kernelReboots parser] ✓ Detected systemd shutdown at line', i + 1, ':', timestamp); + } + continue; + } + + // Pattern 3: "reboot: " messages + if (line.match(/kernel:\s*reboot:/i)) { + const timestamp = this.extractTimestamp(line); + + reboots.push({ + timestamp: timestamp || 'Unknown', + lineNumber: i + 1, + kernelVersion: null, + type: 'reboot_message', + rawLine: line.trim() + }); + + debugLog('[kernelReboots parser] ✓ Detected reboot message at line', i + 1, ':', timestamp); + continue; + } + } + + debugLog('[kernelReboots parser] Found', reboots.length, 'reboot events'); + + return { + count: reboots.length, + events: reboots + }; + }, + + // Helper function to extract timestamp from log line (reuse from liveMigration) + extractTimestamp: function(line) { + // Try ISO timestamp + const isoMatch = line.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?)/); + if (isoMatch) return isoMatch[1]; + + // Try syslog format (Month Day Time) + const syslogMatch = line.match(/((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})/); + if (syslogMatch) return syslogMatch[1]; + + // Try simple date format + const simpleMatch = line.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/); + if (simpleMatch) return simpleMatch[1]; + + return null; + } + }, + + // Rule: Detect Out of Memory (OOM) killer events from message logs + oomKiller: { + // Target file path patterns (same as liveMigration and kernelReboots) + // supportconfig: */messages or */localmessages (with optional suffixes) + // sosreport: */var/log/messages or */sos_commands/logs/journalctl* + filePattern: /\/(messages|localmessages|journalctl[^\/]*)(?:[.-]\d+)?(?:\.txt)?$/, + + // Parse function receives file content as string + // Detects OOM killer events by finding patterns: + // - "Out of memory: Kill process" or "Out of memory: Killed process" + // - "oom-killer:" or "oom_reaper:" + // - Memory statistics and killed process information + // Returns array of detected OOM events with process details + parse: function(content) { + const lines = content.split('\n'); + const oomEvents = []; + + debugLog('[oomKiller parser] Analyzing', lines.length, 'lines for OOM killer events'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Pattern 1: "Out of memory: Kill process" or "Out of memory: Killed process" + const oomKillMatch = line.match(/Out of memory:.*Kill(?:ed)? process\s+(\d+)\s+\(([^)]+)\)/i); + if (oomKillMatch) { + const timestamp = this.extractTimestamp(line); + const pid = oomKillMatch[1]; + const processName = oomKillMatch[2]; + + // Try to find memory score on the same line or nearby lines + let score = null; + const scoreMatch = line.match(/score\s+(\d+)/i); + if (scoreMatch) { + score = scoreMatch[1]; + } + + // Try to find total VM info + let totalVM = null; + const vmMatch = line.match(/total-vm:(\d+)kB/i); + if (vmMatch) { + totalVM = vmMatch[1] + 'kB'; + } + + oomEvents.push({ + timestamp: timestamp || 'Unknown', + lineNumber: i + 1, + pid: pid, + processName: processName, + score: score, + totalVM: totalVM, + type: 'oom_kill', + rawLine: line.trim() + }); + + debugLog('[oomKiller parser] ✓ Detected OOM kill at line', i + 1, ':', timestamp, 'process:', processName, 'pid:', pid); + continue; + } + + // Pattern 2: "oom-killer:" invocation (usually precedes the kill message) + if (line.match(/invoked oom-killer:/i)) { + const timestamp = this.extractTimestamp(line); + + // Extract the process that invoked OOM killer + let invokedBy = null; + const invokeMatch = line.match(/\]\s+([^\s]+)\s+invoked oom-killer:/i); + if (invokeMatch) { + invokedBy = invokeMatch[1]; + } + + // Extract memory allocation info if present + let order = null; + const orderMatch = line.match(/order=(\d+)/i); + if (orderMatch) { + order = orderMatch[1]; + } + + // Check if we already have an event very close to this + const isDuplicate = oomEvents.some(e => { + return Math.abs(e.lineNumber - (i + 1)) < 3; + }); + + if (!isDuplicate) { + oomEvents.push({ + timestamp: timestamp || 'Unknown', + lineNumber: i + 1, + pid: null, + processName: null, + invokedBy: invokedBy, + order: order, + type: 'oom_invoked', + rawLine: line.trim() + }); + + debugLog('[oomKiller parser] ✓ Detected OOM invocation at line', i + 1, ':', timestamp, 'by:', invokedBy); + } + continue; + } + + // Pattern 3: "oom_reaper:" messages (cleanup after OOM kill) + if (line.match(/oom_reaper:/i)) { + const timestamp = this.extractTimestamp(line); + + // Extract PID if present + let pid = null; + const pidMatch = line.match(/reaped process\s+(\d+)/i); + if (pidMatch) { + pid = pidMatch[1]; + } + + // Check if we already have an event very close to this + const isDuplicate = oomEvents.some(e => { + return Math.abs(e.lineNumber - (i + 1)) < 3; + }); + + if (!isDuplicate) { + oomEvents.push({ + timestamp: timestamp || 'Unknown', + lineNumber: i + 1, + pid: pid, + processName: null, + type: 'oom_reaper', + rawLine: line.trim() + }); + + debugLog('[oomKiller parser] ✓ Detected OOM reaper at line', i + 1, ':', timestamp); + } + continue; + } + + // Pattern 4: "Cannot allocate memory" errors + if (line.match(/Cannot allocate memory/i)) { + const timestamp = this.extractTimestamp(line); + + // Extract process name if present + let processName = null; + const processMatch = line.match(/\]\s+([^\s:]+):/); + if (processMatch) { + processName = processMatch[1]; + } + + // Check if we already have an event very close to this + const isDuplicate = oomEvents.some(e => { + return Math.abs(e.lineNumber - (i + 1)) < 3; + }); + + if (!isDuplicate) { + oomEvents.push({ + timestamp: timestamp || 'Unknown', + lineNumber: i + 1, + pid: null, + processName: processName, + type: 'alloc_failure', + rawLine: line.trim() + }); + + debugLog('[oomKiller parser] ✓ Detected allocation failure at line', i + 1, ':', timestamp, 'process:', processName); + } + continue; + } + } + + debugLog('[oomKiller parser] Found', oomEvents.length, 'OOM killer events'); + + return { + count: oomEvents.length, + events: oomEvents + }; + }, + + // Helper function to extract timestamp from log line + extractTimestamp: function(line) { + // Try ISO timestamp + const isoMatch = line.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?)/); + if (isoMatch) return isoMatch[1]; + + // Try syslog format (Month Day Time) + const syslogMatch = line.match(/((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})/); + if (syslogMatch) return syslogMatch[1]; + + // Try simple date format + const simpleMatch = line.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/); + if (simpleMatch) return simpleMatch[1]; + + return null; + } + }, + + // Rule: Validate Azure-specific RPM packages + rpmPackages: { + // Target file patterns + // supportconfig: */rpm.txt + // sosreport: */installed-rpms or */sos_commands/rpm/package-data + filePattern: /\/(rpm\.txt|installed-rpms|package-data)$/, + + // Parse function receives rpm.txt content + // Validates Azure-required packages with specific version requirements + parse: function(content) { + const lines = content.split('\n'); + + debugLog('[rpmPackages parser] Analyzing', lines.length, 'lines'); + + // Required Azure packages with minimum version requirements + const requiredPackages = { + 'fence-agents': { version: '4.4', operator: 'gte' }, + 'python3-azure-mgmt-compute': { version: '17.0', operator: 'gte' }, + 'python3-azure-identity': { version: '1.0', operator: 'gte' }, + 'cloud-netconfig-azure': { version: '1.3', operator: 'gte' }, + 'resource-agents': { version: '4.3', operator: 'gte' }, + 'python3-azure-core': { minVersion: '1.9', maxVersion: '1.22', operator: 'range' } + }; + + const foundPackages = {}; + const warnings = []; + + // Parse RPM listing + // Common RPM formats: + // - "package-name-1.2.3-4.el8.x86_64" + // - "package-name-1.2.3-4.noarch" + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // Try to match RPM package format + // Pattern: package-name-version-release.arch + for (const [pkgName, requirements] of Object.entries(requiredPackages)) { + // Look for package name at start of line + const pkgRegex = new RegExp(`^${pkgName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-(\\d+\\.\\d+(?:\\.\\d+)?)`); + const match = trimmed.match(pkgRegex); + + if (match) { + const version = match[1]; + foundPackages[pkgName] = version; + debugLog('[rpmPackages parser] Found', pkgName, 'version', version); + + // Validate version + if (requirements.operator === 'gte') { + if (!this.compareVersion(version, requirements.version, 'gte')) { + warnings.push({ + package: pkgName, + expected: `>= ${requirements.version}`, + actual: version, + severity: 'error', + message: `Package ${pkgName} version is ${version}, but should be >= ${requirements.version} for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[rpmPackages parser] WARNING:', pkgName, 'version too old'); + } + } else if (requirements.operator === 'range') { + // Check if version is INSIDE the problematic range (inverted logic) + if (this.compareVersion(version, requirements.minVersion, 'gte') && + this.compareVersion(version, requirements.maxVersion, 'lte')) { + warnings.push({ + package: pkgName, + expected: `< ${requirements.minVersion} or > ${requirements.maxVersion}`, + actual: version, + severity: 'error', + message: `Package ${pkgName} version is ${version}, but should be lower than ${requirements.minVersion} or higher than ${requirements.maxVersion} for Azure environments`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[rpmPackages parser] WARNING:', pkgName, 'version in problematic range'); + } + } + } + } + } + + // Check for missing packages + for (const [pkgName, requirements] of Object.entries(requiredPackages)) { + if (!foundPackages[pkgName]) { + warnings.push({ + package: pkgName, + expected: requirements.operator === 'gte' ? `>= ${requirements.version}` : `${requirements.minVersion} - ${requirements.maxVersion}`, + actual: 'not found', + severity: 'error', + message: `Required package ${pkgName} not found in rpm.txt`, + documentationUrl: 'https://learn.microsoft.com/en-us/azure/sap/workloads/high-availability-guide-suse-pacemaker' + }); + debugLog('[rpmPackages parser] WARNING:', pkgName, 'not found'); + } + } + + return { + found: true, + packages: foundPackages, + warnings: warnings + }; + }, + + // Helper function to compare versions + compareVersion: function(actual, expected, operator) { + const parseVersion = (v) => { + const parts = v.split('.').map(p => parseInt(p, 10)); + return { + major: parts[0] || 0, + minor: parts[1] || 0, + patch: parts[2] || 0 + }; + }; + + const actualParts = parseVersion(actual); + const expectedParts = parseVersion(expected); + + switch (operator) { + case 'exact': + return actualParts.major === expectedParts.major && + actualParts.minor === expectedParts.minor; + + case 'gte': // greater than or equal + if (actualParts.major > expectedParts.major) return true; + if (actualParts.major < expectedParts.major) return false; + if (actualParts.minor > expectedParts.minor) return true; + if (actualParts.minor < expectedParts.minor) return false; + return actualParts.patch >= expectedParts.patch; + + case 'lte': // less than or equal + if (actualParts.major < expectedParts.major) return true; + if (actualParts.major > expectedParts.major) return false; + if (actualParts.minor < expectedParts.minor) return true; + if (actualParts.minor > expectedParts.minor) return false; + return actualParts.patch <= expectedParts.patch; + + default: + return false; + } + } + }, + + // Rule: Detect Falcon Sensor (CrowdStrike) and check SAP exceptions + falconSensor: { + // Target file patterns - RPM list and process list + filePattern: /\/(rpm\.txt|installed-rpms|package-data|ps\.txt|ps_.*\.txt)$/, + + parse: function(content) { + const lines = content.split('\n'); + debugLog('[falconSensor parser] Analyzing', lines.length, 'lines for Falcon Sensor'); + + let detected = false; + let version = null; + let runningProcess = false; + const sapExceptions = { + checked: false, + paths: [] + }; + + // Check for Falcon Sensor package or process + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // Check RPM package: falcon-sensor-X.Y.Z + const rpmMatch = trimmed.match(/^falcon-sensor-([\d.]+)/); + if (rpmMatch) { + detected = true; + version = rpmMatch[1]; + debugLog('[falconSensor parser] Found Falcon Sensor RPM:', version); + } + + // Check running process + if (trimmed.includes('falcon-sensor') || trimmed.includes('/opt/CrowdStrike')) { + runningProcess = true; + debugLog('[falconSensor parser] Found Falcon Sensor process'); + } + } + + if (!detected) { + debugLog('[falconSensor parser] Falcon Sensor not detected'); + return { found: false }; + } + + // If detected, check for SAP exclusions in config files + // Common Falcon config locations: /opt/CrowdStrike/falconctl or policy files + // For now, we'll mark as needing manual verification + debugLog('[falconSensor parser] Falcon Sensor detected, version:', version); + + return { + found: true, + version: version, + runningProcess: runningProcess, + sapExceptionsConfigured: null, // null = unknown, needs config file check + message: 'Falcon Sensor detected. SAP exclusions should be verified manually in /opt/CrowdStrike configuration.' + }; + } + }, + + // Rule: Check Falcon Sensor SAP exclusions in config files + falconSensorConfig: { + filePattern: /\/(falconctl|CrowdStrike.*config|falcon.*conf)$/i, + + parse: function(content) { + debugLog('[falconSensorConfig parser] Checking Falcon config for SAP exclusions'); + + // SAP paths that should be excluded + const sapPaths = [ + '/usr/sap', + '/hana/shared', + '/hana/data', + '/hana/log', + '/sapmnt', + '/usr/sap/*/SYS/exe' + ]; + + const foundExclusions = []; + const lines = content.split('\n'); + + for (const line of lines) { + const lower = line.toLowerCase(); + + // Check for exclusion configurations + if (lower.includes('exclude') || lower.includes('exception')) { + for (const sapPath of sapPaths) { + if (line.includes(sapPath)) { + foundExclusions.push(sapPath); + debugLog('[falconSensorConfig parser] Found SAP exclusion:', sapPath); + } + } + } + } + + return { + found: true, + exclusions: foundExclusions, + hasExclusions: foundExclusions.length > 0 + }; + } + }, + + // Rule: Detect Microsoft Defender and check SAP exceptions + msDefender: { + filePattern: /\/(rpm\.txt|installed-rpms|package-data|ps\.txt|ps_.*\.txt)$/, + + parse: function(content) { + const lines = content.split('\n'); + debugLog('[msDefender parser] Analyzing', lines.length, 'lines for MS Defender'); + + let detected = false; + let version = null; + let runningProcess = false; + + // Check for Microsoft Defender package or process + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // Check RPM package: mdatp (Microsoft Defender ATP) + const rpmMatch = trimmed.match(/^mdatp-([\d.]+)/); + if (rpmMatch) { + detected = true; + version = rpmMatch[1]; + debugLog('[msDefender parser] Found MS Defender RPM:', version); + } + + // Check running process + if (trimmed.includes('mdatp') || trimmed.includes('wdavdaemon') || trimmed.includes('/opt/microsoft/mdatp')) { + runningProcess = true; + debugLog('[msDefender parser] Found MS Defender process'); + } + } + + if (!detected) { + debugLog('[msDefender parser] MS Defender not detected'); + return { found: false }; + } + + debugLog('[msDefender parser] MS Defender detected, version:', version); + + return { + found: true, + version: version, + runningProcess: runningProcess, + sapExceptionsConfigured: null, // null = unknown, needs config check + message: 'Microsoft Defender detected. SAP exclusions should be verified with: mdatp exclusion list' + }; + } + }, + + // Rule: Check MS Defender SAP exclusions + msDefenderConfig: { + filePattern: /\/(mdatp.*|defender.*config)$/i, + + parse: function(content) { + debugLog('[msDefenderConfig parser] Checking MS Defender config for SAP exclusions'); + + // SAP paths that should be excluded + const sapPaths = [ + '/usr/sap', + '/hana/shared', + '/hana/data', + '/hana/log', + '/sapmnt', + '/usr/sap/*/SYS/exe' + ]; + + const foundExclusions = []; + const lines = content.split('\n'); + + for (const line of lines) { + const lower = line.toLowerCase(); + + // Check for exclusion configurations + if (lower.includes('exclusion') || lower.includes('exclude')) { + for (const sapPath of sapPaths) { + if (line.includes(sapPath)) { + foundExclusions.push(sapPath); + debugLog('[msDefenderConfig parser] Found SAP exclusion:', sapPath); + } + } + } + } + + return { + found: true, + exclusions: foundExclusions, + hasExclusions: foundExclusions.length > 0 + }; + } + } + + // ADD MORE RULES HERE + // Example: + // systemInfo: { + // filePattern: /\/basic-environment\.txt$/, + // parse: function(content) { + // // Extract system information + // return { /* parsed data */ }; + // } + // } +}; + +// ============================================================================ +// END SCC RULES +// ============================================================================ + +// Load the streaming WASM module +importScripts( + './liblzma-wasm/dist-streaming/liblzma-xz-streaming.js' + CACHE_BUST +); + +let moduleReady = false; +let Module = null; + +// Initialize the WASM module +LZMA_XZ_Streaming_Module({ + locateFile: (path) => { + if (path.endsWith('.wasm')) { + // Return the correct path relative to worker location + const wasmPath = './liblzma-wasm/dist-streaming/liblzma-xz-streaming.wasm' + CACHE_BUST; + debugLog('[XZ Streaming Worker] Loading WASM from:', wasmPath); + return wasmPath; + } + return path; + } +}).then((mod) => { + Module = mod; + moduleReady = true; + debugLog('[XZ Streaming Worker] Module initialized successfully'); + debugLog('[XZ Streaming Worker] Exported functions:', Object.keys(Module).filter(k => k.startsWith('_'))); + // Signal to main thread that worker is ready + self.postMessage({ ready: true }); +}).catch((err) => { + console.error('[XZ Streaming Worker] Module initialization failed:', err); + self.postMessage({ error: 'WASM module initialization failed: ' + err.message }); +}); + +// Parse TAR headers incrementally as data arrives +class IncrementalTARParser { + constructor() { + this.buffer = new Uint8Array(0); + this.files = []; + this.directories = new Set(); + this.fileTypes = {}; + this.offset = 0; + this.totalParsed = 0; + + // SCC report analysis state + this.isSCCReport = false; + this.sccReportName = null; + this.analysisResults = {}; // Stores parsed results by rule name (no raw file content) + } + + // Add decompressed chunk to buffer and parse what we can + addChunk(chunk) { + // Append to buffer + const newBuffer = new Uint8Array(this.buffer.length + chunk.length); + newBuffer.set(this.buffer); + newBuffer.set(chunk, this.buffer.length); + this.buffer = newBuffer; + + // Parse complete TAR entries + this.parseAvailableEntries(); + } + + parseAvailableEntries() { + while (this.buffer.length - this.offset >= 512) { + // Check for end marker (two consecutive zero blocks) + if (this.isEndMarker(this.offset)) { + break; + } + + // Need full header + potential data + const header = this.parseHeader(this.offset); + if (!header) { + break; // Invalid or incomplete header + } + + const entrySize = 512 + Math.ceil(header.size / 512) * 512; + + // Check if we have the complete entry + if (this.buffer.length - this.offset < entrySize) { + break; // Wait for more data + } + + // Process entry + this.processEntry(header); + + // Move to next entry + this.offset += entrySize; + this.totalParsed++; + } + + // Trim processed data from buffer to keep memory low + // Be aggressive about trimming - keep only 512KB of unprocessed data + if (this.offset > 512 * 1024) { + this.buffer = this.buffer.slice(this.offset); + this.offset = 0; + } + } + + isEndMarker(offset) { + if (this.buffer.length - offset < 1024) return false; + for (let i = 0; i < 1024; i++) { + if (this.buffer[offset + i] !== 0) return false; + } + return true; + } + + parseHeader(offset) { + if (this.buffer.length - offset < 512) return null; + + // Read filename (null-terminated) + let filename = ''; + for (let i = 0; i < 100; i++) { + if (this.buffer[offset + i] === 0) break; + filename += String.fromCharCode(this.buffer[offset + i]); + } + + if (!filename) return null; + + // Read size (octal string at offset 124, 12 bytes) + let sizeStr = ''; + for (let i = 124; i < 136; i++) { + const c = this.buffer[offset + i]; + if (c === 0 || c === 32) break; + sizeStr += String.fromCharCode(c); + } + + const size = parseInt(sizeStr.trim(), 8) || 0; + + // Read type flag (offset 156) + const typeflag = this.buffer[offset + 156]; + + return { filename, size, typeflag, offset }; + } + + processEntry(header) { + const { filename, size, typeflag, offset } = header; + + // Detect SCC report using rules + if (!this.isSCCReport && SCC_RULES.detection.isSCCReport(filename)) { + this.isSCCReport = true; + this.sccReportName = filename.split('/')[0]; + debugLog('[TAR Parser] Detected SCC report:', this.sccReportName); + } + + // Process SCC rules if this is an SCC report + if (this.isSCCReport && size > 0) { + this.processSCCRules(filename, size, offset); + } + + // Track directories + if (filename.includes('/')) { + const parts = filename.split('/'); + let path = ''; + for (let i = 0; i < parts.length - 1; i++) { + path += parts[i] + '/'; + this.directories.add(path); + } + } + + // Track file types + const ext = filename.includes('.') ? filename.split('.').pop().toLowerCase() : 'none'; + this.fileTypes[ext] = (this.fileTypes[ext] || 0) + 1; + + // Store file entry + this.files.push({ + name: filename, + size: size, + type: typeflag === 53 ? 'dir' : 'file' + }); + } + + // Process all SCC rules against current file + processSCCRules(filename, size, offset) { + // Skip PaxHeaders - they are TAR extended headers, not actual file content + if (filename.includes('/PaxHeaders/') || filename.endsWith('/PaxHeaders')) { + return; + } + + // Iterate through all rules (except detection) + for (const [ruleName, rule] of Object.entries(SCC_RULES)) { + if (ruleName === 'detection' || !rule.filePattern) continue; + + // Check if filename matches rule pattern + if (rule.filePattern.test(filename)) { + debugLog(`[TAR Parser] Matched rule '${ruleName}' for file:`, filename); + + // Extract file content + const dataOffset = offset + 512; + if (this.buffer.length >= dataOffset + size) { + const content = this.extractFileContent(dataOffset, size); + if (content) { + // For rules that process multiple files (like liveMigration, kernelReboots, and oomKiller) + // we need to accumulate results instead of replacing + const isMultiFileRule = ruleName === 'liveMigration' || ruleName === 'kernelReboots' || ruleName === 'oomKiller'; + + // NOTE: We don't store file content in extractedFiles anymore to save memory + // Content is parsed immediately and discarded + + // Parse using rule's parse function (pass filename for format detection) + try { + const result = rule.parse(content, filename); + + if (isMultiFileRule) { + // Accumulate results for multi-file rules + if (!this.analysisResults[ruleName]) { + this.analysisResults[ruleName] = { + count: 0, + events: [] + }; + } + + // For kernelReboots, deduplicate events based on timestamp and type + if (ruleName === 'kernelReboots') { + let newEventsAdded = 0; + result.events.forEach(newEvent => { + // Check if this event already exists (same timestamp and type) + const isDuplicate = this.analysisResults[ruleName].events.some(existingEvent => { + return existingEvent.timestamp === newEvent.timestamp && + existingEvent.type === newEvent.type && + existingEvent.kernelVersion === newEvent.kernelVersion; + }); + + if (!isDuplicate) { + this.analysisResults[ruleName].events.push({ + ...newEvent, + sourceFile: filename + }); + newEventsAdded++; + } + }); + this.analysisResults[ruleName].count = this.analysisResults[ruleName].events.length; + debugLog(`[TAR Parser] Rule '${ruleName}' accumulated ${newEventsAdded} new events (${result.count - newEventsAdded} duplicates skipped, total: ${this.analysisResults[ruleName].count})`); + } else { + // Merge results without deduplication for other multi-file rules + this.analysisResults[ruleName].count += result.count; + this.analysisResults[ruleName].events.push(...result.events.map(e => ({ + ...e, + sourceFile: filename + }))); + + debugLog(`[TAR Parser] Rule '${ruleName}' accumulated ${result.count} new events (total: ${this.analysisResults[ruleName].count})`); + } + } else { + // Single file rule - for fencingConfig and pacemakerResources, merge/keep best result + if (ruleName === 'fencingConfig') { + // Keep the result with the most information + const existing = this.analysisResults[ruleName]; + + // If no existing result, use this one + if (!existing) { + this.analysisResults[ruleName] = result; + debugLog(`[TAR Parser] Rule '${ruleName}' parsed successfully (first):`, result); + } else { + // Merge: prefer explicit stonithEnabled over null, accumulate devices + const merged = { + found: existing.found || result.found, + stonithEnabled: result.stonithEnabled !== null ? result.stonithEnabled : existing.stonithEnabled, + stonithSourceFile: result.stonithEnabled !== null ? result.stonithSourceFile : existing.stonithSourceFile, + stonithSourcePattern: result.stonithEnabled !== null ? result.stonithSourcePattern : existing.stonithSourcePattern, + fencingDevices: [...existing.fencingDevices], + count: existing.count + }; + + // Add new devices that don't already exist + result.fencingDevices.forEach(newDevice => { + if (!merged.fencingDevices.find(d => d.name === newDevice.name)) { + merged.fencingDevices.push(newDevice); + merged.count++; + } + }); + + this.analysisResults[ruleName] = merged; + debugLog(`[TAR Parser] Rule '${ruleName}' merged with existing (${merged.count} devices, stonith: ${merged.stonithEnabled})`); + } + } else if (ruleName === 'pacemakerResources') { + // Keep the result with resources, or merge resources + const existing = this.analysisResults[ruleName]; + + if (!existing || !existing.found) { + this.analysisResults[ruleName] = result; + debugLog(`[TAR Parser] Rule '${ruleName}' parsed successfully (first):`, result); + } else if (result.found) { + // Merge resources and enrich with node information + let newResourcesAdded = 0; + let enrichedResources = 0; + + result.resources.forEach(newRes => { + const existingRes = existing.resources.find(r => r.name === newRes.name); + if (existingRes) { + // Enrich existing resource with new data (especially node info from crm_mon) + let wasEnriched = false; + if (newRes.node && !existingRes.node) { + existingRes.node = newRes.node; + existingRes.status = newRes.status; + wasEnriched = true; + } + if (newRes.status && !existingRes.status) { + existingRes.status = newRes.status; + wasEnriched = true; + } + if (wasEnriched) { + enrichedResources++; + debugLog(`[TAR Parser] Enriched resource '${newRes.name}' with node/status info`); + } + } else { + // Add new resource only if it's truly new + existing.resources.push(newRes); + newResourcesAdded++; + debugLog(`[TAR Parser] Added new resource '${newRes.name}'`); + } + }); + existing.count = existing.resources.length; + debugLog(`[TAR Parser] Rule '${ruleName}' merged: ${newResourcesAdded} new resources added, ${enrichedResources} enriched, ${result.resources.length - newResourcesAdded - enrichedResources} duplicates skipped (${existing.count} total)`); + } + } else if (ruleName === 'clusterNodes') { + // Merge and deduplicate cluster nodes from multiple files + const existing = this.analysisResults[ruleName]; + + if (!existing || !existing.nodes || existing.nodes.length === 0) { + this.analysisResults[ruleName] = result; + debugLog(`[TAR Parser] Rule '${ruleName}' parsed successfully (first): ${result.nodes.length} nodes`); + } else { + // Merge node lists and deduplicate + const nodeSet = new Set([...existing.nodes, ...result.nodes]); + // Merge node-to-IP mappings + const mergedMap = {...existing.nodeToIpMap, ...result.nodeToIpMap}; + this.analysisResults[ruleName] = { + nodes: Array.from(nodeSet).sort(), + nodeToIpMap: mergedMap + }; + debugLog(`[TAR Parser] Rule '${ruleName}' merged (${this.analysisResults[ruleName].nodes.length} unique nodes, ${Object.keys(mergedMap).length} mappings)`); + } + } else { + // Other single file rules - replace result + this.analysisResults[ruleName] = result; + debugLog(`[TAR Parser] Rule '${ruleName}' parsed successfully:`, result); + } + } + } catch (e) { + console.error(`[TAR Parser] Rule '${ruleName}' parse failed:`, e); + } + } + } + } + } + } + + extractFileContent(offset, size) { + const contentBytes = this.buffer.slice(offset, offset + size); + try { + return new TextDecoder('utf-8').decode(contentBytes); + } catch (e) { + console.error('[TAR Parser] Failed to decode file content:', e); + return null; + } + } + + getAnalysis() { + // Get raw cluster nodes (may be IPs or hostnames) + const clusterNodesData = this.analysisResults.clusterNodes || { nodes: [], nodeToIpMap: {} }; + let clusterNodes = clusterNodesData.nodes || []; + const nodeToIpMap = clusterNodesData.nodeToIpMap || {}; + const hostsData = this.analysisResults.hostsFile || null; + const liveMigrationData = this.analysisResults.liveMigration || null; + const kernelRebootsData = this.analysisResults.kernelReboots || null; + const oomKillerData = this.analysisResults.oomKiller || null; + const corosyncData = this.analysisResults.corosyncConfig || null; + const rpmPackagesData = this.analysisResults.rpmPackages || null; + const pacemakerResourcesData = this.analysisResults.pacemakerResources || null; + const fencingConfigData = this.analysisResults.fencingConfig || null; + const clusterEventsData = this.analysisResults.clusterEvents || null; + + // Antivirus detection results + const falconSensorData = this.analysisResults.falconSensor || { found: false }; + const falconConfigData = this.analysisResults.falconSensorConfig || { found: false }; + const msDefenderData = this.analysisResults.msDefender || { found: false }; + const msDefenderConfigData = this.analysisResults.msDefenderConfig || { found: false }; + + // Combine antivirus results + const antivirusResults = { + falconSensor: { + detected: falconSensorData.found, + version: falconSensorData.version, + runningProcess: falconSensorData.runningProcess, + sapExceptions: falconConfigData.hasExclusions || false, + exclusionPaths: falconConfigData.exclusions || [], + message: falconSensorData.message + }, + msDefender: { + detected: msDefenderData.found, + version: msDefenderData.version, + runningProcess: msDefenderData.runningProcess, + sapExceptions: msDefenderConfigData.hasExclusions || false, + exclusionPaths: msDefenderConfigData.exclusions || [], + message: msDefenderData.message + }, + // Overall status + anyDetected: falconSensorData.found || msDefenderData.found, + allHaveExceptions: (falconSensorData.found ? (falconConfigData.hasExclusions || false) : true) && + (msDefenderData.found ? (msDefenderConfigData.hasExclusions || false) : true) + }; + + debugLog('[TAR Parser] getAnalysis() called'); + debugLog('[TAR Parser] analysisResults:', this.analysisResults); + debugLog('[TAR Parser] Raw cluster nodes:', clusterNodes); + debugLog('[TAR Parser] Node-to-IP mappings:', nodeToIpMap); + + // Resolve cluster nodes using multiple strategies: + // 1. If we have nodeToIpMap (from corosync.conf), use it to resolve IPs back to hostnames + // 2. Then use hosts file to resolve any remaining IPs to hostnames + // 3. Remove duplicates (IPs that have corresponding hostnames in the same list) + if (clusterNodes.length > 0) { + const resolvedNodes = []; + const ipToHostnameMap = {}; // Reverse mapping: IP -> hostname + + // Build reverse map from nodeToIpMap + for (const [hostname, ip] of Object.entries(nodeToIpMap)) { + ipToHostnameMap[ip] = hostname; + } + + clusterNodes.forEach(node => { + // Check if it's an IP address + if (node.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { + // First, check if we have a hostname mapping from corosync.conf + if (ipToHostnameMap[node]) { + const hostname = ipToHostnameMap[node]; + debugLog(`[TAR Parser] Resolved IP ${node} to hostname ${hostname} (from corosync nodeToIpMap)`); + resolvedNodes.push(hostname); + } + // If not, try to resolve using hosts file + else if (hostsData) { + const hostEntry = hostsData.entries.find(entry => entry.ip === node); + if (hostEntry && hostEntry.hostnames.length > 0) { + // Use the first hostname + const hostname = hostEntry.hostnames[0]; + debugLog(`[TAR Parser] Resolved IP ${node} to hostname ${hostname} (from hosts file)`); + resolvedNodes.push(hostname); + } else { + // Keep the IP if we can't resolve it + debugLog(`[TAR Parser] Could not resolve IP ${node}, keeping as-is`); + resolvedNodes.push(node); + } + } else { + resolvedNodes.push(node); + } + } else { + // It's a hostname - check if we have an IP mapping for validation + if (nodeToIpMap[node]) { + debugLog(`[TAR Parser] Hostname ${node} maps to IP ${nodeToIpMap[node]} (from corosync.conf)`); + } + resolvedNodes.push(node); + } + }); + + // Deduplicate resolved nodes (in case same hostname was added multiple times) + clusterNodes = Array.from(new Set(resolvedNodes)).sort(); + debugLog('[TAR Parser] Resolved cluster nodes (deduplicated):', clusterNodes); + } + + let nodesInHosts = []; + let nodesMissingFromHosts = []; + + if (hostsData && clusterNodes.length > 0) { + const hostsSet = new Set(hostsData.allHostnames); + const hostsIpSet = new Set(hostsData.entries.map(e => e.ip)); + + clusterNodes.forEach(node => { + let foundInHosts = false; + + // Check if the node (hostname) is directly in hosts file + if (hostsSet.has(node)) { + foundInHosts = true; + debugLog(`[TAR Parser] Node ${node} found directly in hosts file`); + } + // Check if we have an IP mapping for this hostname and that IP is in hosts file + else if (nodeToIpMap[node] && hostsIpSet.has(nodeToIpMap[node])) { + foundInHosts = true; + debugLog(`[TAR Parser] Node ${node} found in hosts file via IP ${nodeToIpMap[node]}`); + } + + if (foundInHosts) { + nodesInHosts.push(node); + } else { + nodesMissingFromHosts.push(node); + } + }); + + debugLog('[TAR Parser] Nodes found in hosts file:', nodesInHosts); + debugLog('[TAR Parser] Nodes missing from hosts file:', nodesMissingFromHosts); + } + + return { + fileCount: this.files.length, + directories: Array.from(this.directories).sort(), + fileTypes: this.fileTypes, + files: this.files.slice(0, 50), // Return first 50 files + totalParsed: this.totalParsed, + // SCC report information + isSCCReport: this.isSCCReport, + sccReportName: this.sccReportName, + // Rule-based analysis results + azureVMProperties: this.analysisResults.azureVMProperties || null, + osRelease: this.analysisResults.osRelease || null, + clusterNodes: clusterNodes, + nodeToIpMap: nodeToIpMap, + hostsFile: hostsData, + liveMigration: liveMigrationData, + kernelReboots: kernelRebootsData, + oomKiller: oomKillerData, + corosyncConfig: corosyncData, + corosyncStatus: this.analysisResults.corosyncStatus || null, + rpmPackages: rpmPackagesData, + pacemakerResources: pacemakerResourcesData, + fencingConfig: fencingConfigData, + clusterEvents: clusterEventsData, + antivirus: antivirusResults, + // Cross-validation results + nodesInHosts: nodesInHosts, + nodesMissingFromHosts: nodesMissingFromHosts + }; + } + + finish() { + // Parse any remaining complete entries + this.parseAvailableEntries(); + return this.getAnalysis(); + } +} + +// Handle messages from main thread +self.onmessage = async function(e) { + // Handle debug mode setting + if (e.data.command === 'set_debug') { + DEBUG_MODE = e.data.enabled; + debugLog(`[Worker] Debug mode ${DEBUG_MODE ? 'enabled' : 'disabled'}`); + return; + } + + // Handle TAR-only analysis (for already decompressed data like from gzip) + if (e.data.cmd === 'analyze_tar') { + try { + const { tarData } = e.data; + debugLog(`[Worker] Starting TAR analysis: ${tarData.byteLength} bytes`); + + // Initialize TAR parser + const tarParser = new IncrementalTARParser(); + + // Process the TAR data + const tarBytes = new Uint8Array(tarData); + tarParser.addChunk(tarBytes); + + // Get analysis results + const analysis = tarParser.getAnalysis(); + + debugLog(`[Worker] TAR analysis complete: ${analysis.fileCount} files`); + + try { + self.postMessage({ + success: true, + analysis: analysis, + progress: 100 + }); + debugLog('[Worker] Success message sent to main thread'); + } catch (err) { + console.error('[Worker] Error sending message:', err); + self.postMessage({ error: 'Failed to send results: ' + err.message }); + } + + return; + } catch (err) { + console.error('[Worker] TAR analysis error:', err); + self.postMessage({ error: 'TAR analysis failed: ' + err.message }); + return; + } + } + + // Handle streaming XZ decompression (existing code) + if (e.data.cmd === 'decompress_streaming') { + if (!moduleReady) { + self.postMessage({ error: 'Module not ready' }); + return; + } + + try { + const { compressedData, chunkSize } = e.data; + const inputSize = compressedData.byteLength; + const effectiveChunkSize = chunkSize || (256 * 1024); // 256KB default + + debugLog(`[XZ Streaming Worker] Starting streaming decompression: ${inputSize} bytes input`); + + // Initialize streaming decoder + const errBufSize = 256; + const errBuf = Module._malloc(errBufSize); + const handle = Module._xz_stream_init(errBuf, errBufSize); + + if (!handle) { + const errMsg = Module.UTF8ToString(errBuf); + Module._free(errBuf); + throw new Error(`Failed to initialize stream: ${errMsg}`); + } + + debugLog('[XZ Streaming Worker] Stream initialized'); + + // Initialize TAR parser + const tarParser = new IncrementalTARParser(); + + // Process input in chunks + let inputOffset = 0; + let totalDecompressed = 0; + let chunkCount = 0; + + while (inputOffset < inputSize) { + // Get next chunk of input + const remainingInput = inputSize - inputOffset; + const currentChunkSize = Math.min(effectiveChunkSize, remainingInput); + const isLastChunk = (inputOffset + currentChunkSize >= inputSize); + + // Copy input chunk to WASM memory + const inputPtr = Module._malloc(currentChunkSize); + Module.HEAPU8.set( + new Uint8Array(compressedData, inputOffset, currentChunkSize), + inputPtr + ); + + // Allocate status and output length variables + const outLenPtr = Module._malloc(4); + const statusPtr = Module._malloc(4); + + // Process chunk + const outputPtr = Module._xz_stream_process( + handle, + inputPtr, + currentChunkSize, + outLenPtr, + statusPtr + ); + + const outLen = Module.getValue(outLenPtr, 'i32'); + const status = Module.getValue(statusPtr, 'i32'); + + debugLog(`[XZ Streaming Worker] Chunk ${chunkCount}: input=${currentChunkSize}, output=${outLen}, status=${status}`); + + // Free input and status buffers + Module._free(inputPtr); + Module._free(outLenPtr); + Module._free(statusPtr); + + // Check for output + if (outputPtr && outLen > 0) { + // Copy output data to a new buffer + const outputData = new Uint8Array(outLen); + outputData.set(Module.HEAPU8.subarray(outputPtr, outputPtr + outLen)); + + // Free WASM memory immediately + Module._free(outputPtr); + + // Feed to TAR parser + tarParser.addChunk(outputData); + totalDecompressed += outLen; + + // Send progress update + const progress = Math.floor((inputOffset / inputSize) * 100); + self.postMessage({ + progress, + decompressed: totalDecompressed, + analysis: tarParser.getAnalysis() + }); + + // Hint to GC that outputData can be collected + // (it's been processed by tarParser.addChunk) + } else if (outputPtr) { + // Free even if no data + Module._free(outputPtr); + } + + // Check status + if (status < 0) { + const errMsg = Module.UTF8ToString(Module._xz_stream_error(handle)); + Module._xz_stream_free(handle); + Module._free(errBuf); + throw new Error(`Decompression error: ${errMsg} (status=${status})`); + } + + if (status === 1) { + // Stream finished + debugLog('[XZ Streaming Worker] Stream finished'); + break; + } + + // Move to next chunk + inputOffset += currentChunkSize; + chunkCount++; + + // Yield to prevent blocking and allow GC + if (chunkCount % 10 === 0) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + + // More aggressive GC hint every 50 chunks (helps browser reclaim memory) + if (chunkCount % 50 === 0) { + await new Promise(resolve => setTimeout(resolve, 10)); + } + } + + // Cleanup + Module._xz_stream_free(handle); + Module._free(errBuf); + + // Final analysis + const finalAnalysis = tarParser.finish(); + + debugLog(`[XZ Streaming Worker] Complete: ${totalDecompressed} bytes decompressed, ${finalAnalysis.fileCount} files`); + + self.postMessage({ + success: true, + totalDecompressed, + analysis: finalAnalysis + }); + + } catch (error) { + console.error('[XZ Streaming Worker] Error:', error); + self.postMessage({ + error: error.message || 'Unknown streaming decompression error' + }); + } + } +}; + +debugLog('[XZ Streaming Worker] Worker initialized, waiting for module...'); diff --git a/rca-tool/liblzma-wasm/build-liblzma-streaming.sh b/rca-tool/liblzma-wasm/build-liblzma-streaming.sh new file mode 100755 index 0000000..0650e07 --- /dev/null +++ b/rca-tool/liblzma-wasm/build-liblzma-streaming.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -e + +echo "=== Building Streaming liblzma WASM ===" + +# Directories +SRC_DIR="xz-5.8.1" +BUILD_DIR="build-streaming" +DIST_DIR="dist-streaming" + +# Clean previous build +rm -rf "$BUILD_DIR" "$DIST_DIR" +mkdir -p "$BUILD_DIR" "$DIST_DIR" + +# Download and extract XZ Utils if needed +if [ ! -d "$SRC_DIR" ]; then + echo "Downloading XZ Utils 5.8.1..." + wget -q https://github.com/tukaani-project/xz/releases/download/v5.8.1/xz-5.8.1.tar.xz + tar -xJf xz-5.8.1.tar.xz + rm xz-5.8.1.tar.xz +fi + +# Configure and build liblzma +echo "Configuring liblzma with emscripten..." +cd "$SRC_DIR" +emconfigure ./configure \ + --disable-shared \ + --enable-static \ + --disable-xz \ + --disable-xzdec \ + --disable-lzmadec \ + --disable-lzmainfo \ + --disable-lzma-links \ + --disable-scripts \ + --disable-doc \ + --prefix="$(pwd)/../$BUILD_DIR" + +echo "Building liblzma..." +emmake make -j$(nproc) +emmake make install + +cd .. + +echo "Compiling streaming wrapper with emcc..." +emcc \ + -O3 \ + -s WASM=1 \ + -s MODULARIZE=1 \ + -s EXPORT_NAME='LZMA_XZ_Streaming_Module' \ + -s EXPORTED_FUNCTIONS='["_malloc","_free","_xz_stream_init","_xz_stream_process","_xz_stream_error","_xz_stream_free","_xz_decompress"]' \ + -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","getValue","setValue","UTF8ToString"]' \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s INITIAL_MEMORY=16MB \ + -s MAXIMUM_MEMORY=2GB \ + -I"$BUILD_DIR/include" \ + xz_wrapper_streaming.c \ + "$BUILD_DIR/lib/liblzma.a" \ + -o "$DIST_DIR/liblzma-xz-streaming.js" + +echo "=== Build Complete ===" +echo "Output files:" +ls -lh "$DIST_DIR/" +echo "" +echo "Streaming WASM module ready at: $DIST_DIR/liblzma-xz-streaming.{js,wasm}" diff --git a/rca-tool/styles.css b/rca-tool/styles.css new file mode 100644 index 0000000..a776d3d --- /dev/null +++ b/rca-tool/styles.css @@ -0,0 +1,419 @@ +/* CSS Variables for Light and Dark Mode */ +:root { + --bg-color: #f0f0f0; + --text-color: #333; + --card-bg: white; + --card-shadow: rgba(0,0,0,0.1); + --accent-color: #007bff; + --muted-text: #666; + --border-color: #ccc; + + /* Semantic colors */ + --success-color: #28a745; + --danger-color: #dc3545; + --warning-color: #ffc107; + --info-color: #17a2b8; + --warning-text: #856404; + --danger-dark: #721c24; + --info-dark: #0c5460; + + /* Code and inline elements */ + --code-bg: #f8f9fa; + --code-border: #e9ecef; + --progress-bg: #f0f0f0; + + /* Drop zone colors */ + --dropzone-bg: #e3f2fd; + --dropzone-border: #007bff; + --dropzone-bg-active: #cce5ff; +} + +body.dark-mode { + --bg-color: #1a1a1a; + --text-color: #e0e0e0; + --card-bg: #2d2d2d; + --card-shadow: rgba(0,0,0,0.3); + --accent-color: #4a9eff; + --muted-text: #aaa; + --border-color: #444; + + /* Semantic colors - adjusted for dark mode */ + --success-color: #3cb371; + --danger-color: #e57373; + --warning-color: #ffb74d; + --info-color: #4fc3f7; + --warning-text: #ffb74d; + --danger-dark: #ef5350; + --info-dark: #29b6f6; + + /* Code and inline elements */ + --code-bg: #3a3a3a; + --code-border: #4a4a4a; + --progress-bg: #2d2d2d; + + /* Drop zone colors */ + --dropzone-bg: #1e3a5f; + --dropzone-border: #4a9eff; + --dropzone-bg-active: #2a4a7f; +} + +/* Base Styles */ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: var(--bg-color); + color: var(--text-color); + transition: background-color 0.3s, color 0.3s; +} + +h1 { + text-align: center; + color: var(--text-color); + margin-bottom: 30px; +} + +.container { + max-width: 800px; + margin: 0 auto; + position: relative; +} + +/* Info Box Styles */ +.info { + background: var(--card-bg); + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; + box-shadow: 0 2px 5px var(--card-shadow); + transition: background-color 0.3s, box-shadow 0.3s; +} + +.info h3 { + margin-top: 0; + color: var(--accent-color); +} + +.info ul { + margin: 10px 0; + padding-left: 20px; +} + +/* Loading Indicator */ +#loading { + text-align: center; + padding: 20px; + color: var(--muted-text); +} + +/* Theme Toggle Button */ +#theme-toggle { + position: fixed; + top: 20px; + right: 20px; + width: 50px; + height: 50px; + border-radius: 50%; + border: 2px solid var(--border-color); + background: var(--card-bg); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + box-shadow: 0 2px 5px var(--card-shadow); + transition: all 0.3s; + z-index: 1000; +} + +#theme-toggle:hover { + transform: scale(1.1); +} + +/* Drop Zone Styles */ +#drop-zone { + border: 2px dashed var(--border-color); + border-radius: 10px; + width: 500px; + max-width: 90%; + min-height: 200px; + text-align: center; + padding: 20px; + margin: 20px auto; + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: var(--card-bg); + transition: background-color 0.3s, border-color 0.3s; +} + +#drop-zone h2 { + color: var(--text-color); + margin: 0 0 10px 0; +} + +#drop-zone p { + color: var(--text-color); + margin: 5px 0; +} + +/* File Input Styles */ +#file-input { + padding: 12px; + margin: 15px; + font-size: 16px; + cursor: pointer; + border: 2px solid var(--accent-color); + border-radius: 5px; + background: var(--card-bg); + color: var(--text-color); + transition: all 0.3s; +} + +/* Content Block Styles - Dark Mode Variables */ +body.dark-mode { + --success-bg: #1e4620; + --success-text: #a8d5a8; + --success-border: #4caf50; + --warning-bg: #5a4a1f; + --warning-text: #f5d98f; + --warning-border: #ffc107; + --danger-bg: #4a1f1f; + --danger-text: #f5a8a8; + --danger-border: #dc3545; + --info-bg: #1f3a4a; + --info-text: #a8c8d8; + --info-border: #17a2b8; + --content-bg: #2d2d2d; + --content-border: #444; +} + +/* Content Block Styles - Light Mode Variables */ +body:not(.dark-mode) { + --success-bg: #d4edda; + --success-text: #155724; + --success-border: #28a745; + --warning-bg: #fff3cd; + --warning-text: #856404; + --warning-border: #ffc107; + --danger-bg: #f8d7da; + --danger-text: #721c24; + --danger-border: #dc3545; + --info-bg: #d1ecf1; + --info-text: #0c5460; + --info-border: #17a2b8; + --content-bg: white; + --content-border: #dee2e6; +} + +/* Success Block */ +.success-block { + background: var(--success-bg, #d4edda); + color: var(--success-text, #155724); + padding: 15px; + border-radius: 5px; + border-left: 4px solid var(--success-border, #28a745); + margin-bottom: 15px; +} + +/* Warning Block */ +.warning-block { + background: var(--warning-bg); + color: var(--warning-text); + padding: 15px; + border-radius: 5px; + border-left: 4px solid var(--warning-border); + margin-bottom: 15px; +} + +/* Danger Block */ +.danger-block { + background: var(--danger-bg); + color: var(--danger-text); + padding: 15px; + border-radius: 5px; + border-left: 4px solid var(--danger-border); + margin-bottom: 15px; +} + +/* Info Block */ +.info-block { + background: var(--info-bg); + color: var(--info-text); + padding: 15px; + border-radius: 5px; + border-left: 4px solid var(--info-border); + margin-bottom: 15px; +} + +/* Content Details */ +.content-details { + background: var(--content-bg); + padding: 15px; + border-radius: 5px; + border: 1px solid var(--content-border); + margin-bottom: 15px; +} + +.content-details summary { + cursor: pointer; + font-weight: bold; + user-select: none; + color: var(--text-color); +} + +/* Utility Classes for Text Colors */ +.text-success { color: var(--success-color); } +.text-danger { color: var(--danger-color); } +.text-warning { color: var(--warning-color); } +.text-info { color: var(--info-color); } +.text-muted { color: var(--muted-text); } +.text-warning-dark { color: var(--warning-text); } +.text-danger-dark { color: var(--danger-dark); } +.text-info-dark { color: var(--info-dark); } + +/* Utility Classes for Backgrounds */ +.bg-code { background: var(--code-bg); } +.bg-card { background: var(--card-bg); } + +/* Code Elements */ +code { + background: var(--code-bg); + padding: 2px 6px; + border-radius: 3px; + font-family: monospace; + font-size: 0.9em; +} + +/* Badge Styles */ +.badge { + padding: 1px 5px; + border-radius: 3px; + font-size: 11px; + margin-left: 5px; + display: inline-block; +} + +.badge-success { + background: var(--success-color); + color: white; +} + +.badge-danger { + background: var(--danger-color); + color: white; +} + +.badge-warning { + background: var(--warning-color); + color: #000; +} + +.badge-info { + background: var(--info-color); + color: white; +} + +/* Progress Bar */ +.progress-container { + text-align: center; + padding: 10px; +} + +.progress-title { + color: var(--accent-color); + margin: 5px 0; + font-weight: bold; +} + +.progress-bar-wrapper { + margin: 10px auto; + max-width: 400px; +} + +.progress-bar { + width: 100%; + background: var(--progress-bg); + border-radius: 5px; + padding: 2px; +} + +.progress-fill { + width: 0%; + background: var(--accent-color); + height: 12px; + border-radius: 3px; + transition: width 0.3s; +} + +.progress-text { + font-size: 12px; + color: var(--muted-text); + margin: 5px 0; +} + +/* List Item Styles */ +.event-item { + margin: 8px 0; + font-size: 13px; + border-bottom: 1px solid var(--code-border); + padding-bottom: 8px; +} + +.event-timestamp { + font-weight: bold; + font-size: 12px; + margin-bottom: 3px; +} + +.event-details { + font-size: 11px; + color: var(--muted-text); + margin-top: 3px; +} + +.event-raw-line { + font-size: 10px; + color: var(--muted-text); + font-family: monospace; + display: block; + margin-top: 4px; + padding: 4px; + background: var(--code-bg); + border-radius: 2px; + overflow-x: auto; + white-space: nowrap; +} + +/* Subsection Styles */ +.subsection { + margin-top: 15px; + padding: 10px; + background: var(--card-bg); + border-radius: 3px; +} + +.subsection-danger { + margin-top: 15px; + padding: 10px; + background: var(--card-bg); + border-radius: 3px; + border-left: 3px solid var(--danger-color); +} + +/* Warning Box */ +.warning-box { + margin-top: 15px; + padding: 12px; + background: var(--card-bg); + border-radius: 3px; + border-left: 3px solid var(--danger-color); + max-width: 100%; + box-sizing: border-box; + overflow-wrap: break-word; + word-break: break-word; + white-space: normal; +} + From 448c53cb23c980e2fe9d897ff8c984be82e20c94 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Wed, 19 Nov 2025 12:57:54 -0600 Subject: [PATCH 002/145] Adding rca-tool branch to deploy --- rca-tool/.github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rca-tool/.github/workflows/deploy.yml b/rca-tool/.github/workflows/deploy.yml index 70b9253..a5b0aa3 100644 --- a/rca-tool/.github/workflows/deploy.yml +++ b/rca-tool/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Build and Deploy to GitHub Pages on: push: - branches: [ main, master ] + branches: [ main, master, rca-tool ] workflow_dispatch: permissions: From 96413212a571ae0d681fc6dd041d387945feaa36 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Wed, 19 Nov 2025 12:58:59 -0600 Subject: [PATCH 003/145] Moving .github to root of tree --- {rca-tool/.github => .github}/workflows/deploy.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {rca-tool/.github => .github}/workflows/deploy.yml (100%) diff --git a/rca-tool/.github/workflows/deploy.yml b/.github/workflows/deploy.yml similarity index 100% rename from rca-tool/.github/workflows/deploy.yml rename to .github/workflows/deploy.yml From 6e82c443d3c4f40482cf9d6d0e0f9d6af145a369 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Wed, 19 Nov 2025 13:01:40 -0600 Subject: [PATCH 004/145] Fixing paths for website deploy --- .github/workflows/deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a5b0aa3..32b5fe7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -33,12 +33,13 @@ jobs: - name: Build WASM module run: | - cd liblzma-wasm + cd rca-too/liblzma-wasm chmod +x build-liblzma-streaming.sh ./build-liblzma-streaming.sh - name: Prepare deployment directory run: | + cd rca-tool/ mkdir -p _site # Copy HTML, CSS and worker JS from root cp index.html _site/ @@ -47,13 +48,12 @@ jobs: # Copy built WASM files cp liblzma-wasm/dist-streaming/liblzma-xz-streaming.js _site/ cp liblzma-wasm/dist-streaming/liblzma-xz-streaming.wasm _site/ - # Optional: Copy README if you want it accessible cp README.md _site/ || true - name: List deployment files run: | echo "Files to be deployed:" - ls -lh _site/ + ls -lh rca-tool/_site/ - name: Setup Pages uses: actions/configure-pages@v4 @@ -61,7 +61,7 @@ jobs: - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: '_site' + path: 'rca-tool/_site' deploy: environment: From 3502986e19cff86a7da253d1ec58d8626a5630d0 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Wed, 19 Nov 2025 13:03:58 -0600 Subject: [PATCH 005/145] Fixing paths for website deploy --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 32b5fe7..f4211d3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -33,7 +33,7 @@ jobs: - name: Build WASM module run: | - cd rca-too/liblzma-wasm + cd rca-tool/liblzma-wasm chmod +x build-liblzma-streaming.sh ./build-liblzma-streaming.sh From d6eb960f7da2ce61027438d910ec0d6745865682 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Wed, 19 Nov 2025 13:09:48 -0600 Subject: [PATCH 006/145] Adding c file missing in build --- rca-tool/liblzma-wasm/xz_wrapper_streaming.c | 273 +++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 rca-tool/liblzma-wasm/xz_wrapper_streaming.c diff --git a/rca-tool/liblzma-wasm/xz_wrapper_streaming.c b/rca-tool/liblzma-wasm/xz_wrapper_streaming.c new file mode 100644 index 0000000..51a7439 --- /dev/null +++ b/rca-tool/liblzma-wasm/xz_wrapper_streaming.c @@ -0,0 +1,273 @@ +#include +#include +#include +#include + +// Streaming XZ decompressor that processes chunks without loading entire output +// Maximum chunk size for output (keeps memory bounded) +#define CHUNK_SIZE (1024 * 1024) // 1MB chunks +#define INPUT_BUFFER_SIZE (256 * 1024) // 256KB input buffer + +// Opaque handle for streaming state +typedef struct { + lzma_stream strm; + int finished; + int error_code; + char error_msg[256]; +} xz_stream_state; + +// Initialize a streaming XZ decompressor +// Returns handle (opaque pointer) or NULL on failure +void* xz_stream_init(char *err_buf, size_t err_cap) { + xz_stream_state *state = (xz_stream_state*) calloc(1, sizeof(xz_stream_state)); + if (!state) { + if (err_buf && err_cap) strncpy(err_buf, "state alloc failed", err_cap-1); + return NULL; + } + + state->strm = (lzma_stream)LZMA_STREAM_INIT; + lzma_ret ret = lzma_stream_decoder(&state->strm, UINT64_MAX, LZMA_CONCATENATED); + if (ret != LZMA_OK) { + if (err_buf && err_cap) strncpy(err_buf, "decoder init failed", err_cap-1); + free(state); + return NULL; + } + + state->finished = 0; + state->error_code = 0; + state->error_msg[0] = '\0'; + return (void*)state; +} + +// Process a chunk of compressed input +// Returns: allocated output buffer (caller must free via free()), or NULL if no output yet or error +// Sets *out_len to output size (0 if no output) +// Sets *status: 0=need more input, 1=stream finished, <0=error +uint8_t* xz_stream_process(void *handle, const uint8_t *in, size_t in_len, size_t *out_len, int *status) { + if (!handle || !out_len || !status) { + if (status) *status = -1; + if (out_len) *out_len = 0; + return NULL; + } + + xz_stream_state *state = (xz_stream_state*)handle; + *out_len = 0; + *status = 0; + + if (state->finished) { + *status = 1; + return NULL; + } + + if (state->error_code) { + *status = state->error_code; + return NULL; + } + + // Allocate output buffer for this chunk (2MB to handle larger decompressed chunks) + size_t out_capacity = CHUNK_SIZE * 2; + uint8_t *out_buf = (uint8_t*) malloc(out_capacity); + if (!out_buf) { + state->error_code = -2; + strncpy(state->error_msg, "output buffer alloc failed", sizeof(state->error_msg)-1); + *status = -2; + return NULL; + } + + // Set input + state->strm.next_in = in; + state->strm.avail_in = in_len; + + // Set output + state->strm.next_out = out_buf; + state->strm.avail_out = out_capacity; + + size_t total_output = 0; + + // Keep decoding while we have input OR while the decoder produces output + // This handles cases where one compressed chunk expands to multiple output chunks + lzma_action action = (in_len == 0) ? LZMA_FINISH : LZMA_RUN; + lzma_ret ret; + + do { + size_t out_before = state->strm.avail_out; + ret = lzma_code(&state->strm, action); + size_t produced = out_before - state->strm.avail_out; + total_output += produced; + + if (ret == LZMA_STREAM_END) { + state->finished = 1; + *out_len = total_output; + *status = 1; // finished + if (total_output == 0) { + free(out_buf); + return NULL; + } + return out_buf; + } else if (ret == LZMA_OK) { + // Continue if output buffer is full (more data available) + // or if we consumed input but haven't exhausted the internal buffers + if (state->strm.avail_out == 0) { + // Need to expand output buffer + out_capacity *= 2; + uint8_t *new_buf = (uint8_t*) realloc(out_buf, out_capacity); + if (!new_buf) { + state->error_code = -2; + strncpy(state->error_msg, "output buffer realloc failed", sizeof(state->error_msg)-1); + *status = -2; + free(out_buf); + return NULL; + } + out_buf = new_buf; + state->strm.next_out = out_buf + total_output; + state->strm.avail_out = out_capacity - total_output; + } else { + // No more output available, need more input + break; + } + } else { + // Error + state->error_code = -(int)ret; + // Error + state->error_code = -(int)ret; + switch (ret) { + case LZMA_MEM_ERROR: + strncpy(state->error_msg, "memory error", sizeof(state->error_msg)-1); + break; + case LZMA_MEMLIMIT_ERROR: + strncpy(state->error_msg, "memory limit", sizeof(state->error_msg)-1); + break; + case LZMA_FORMAT_ERROR: + strncpy(state->error_msg, "format error", sizeof(state->error_msg)-1); + break; + case LZMA_OPTIONS_ERROR: + strncpy(state->error_msg, "options error", sizeof(state->error_msg)-1); + break; + case LZMA_DATA_ERROR: + strncpy(state->error_msg, "data error", sizeof(state->error_msg)-1); + break; + case LZMA_BUF_ERROR: + strncpy(state->error_msg, "buffer error", sizeof(state->error_msg)-1); + break; + default: + strncpy(state->error_msg, "decode error", sizeof(state->error_msg)-1); + break; + } + *status = state->error_code; + free(out_buf); + return NULL; + } + } while (ret == LZMA_OK); + + // Return output if any + *out_len = total_output; + *status = 0; // need more input + if (total_output == 0) { + free(out_buf); + return NULL; + } + return out_buf; +} + +// Get error message from state +const char* xz_stream_error(void *handle) { + if (!handle) return "invalid handle"; + xz_stream_state *state = (xz_stream_state*)handle; + return state->error_msg[0] ? state->error_msg : "no error"; +} + +// Clean up streaming state +void xz_stream_free(void *handle) { + if (!handle) return; + xz_stream_state *state = (xz_stream_state*)handle; + lzma_end(&state->strm); + free(state); +} + +// Original non-streaming function (kept for backward compatibility) +int xz_decompress(const uint8_t *in, size_t in_len, uint8_t **out_ptr, size_t *out_len, char *err_buf, size_t err_cap) { + if (!in || !out_ptr || !out_len) { + if (err_buf && err_cap) strncpy(err_buf, "invalid arguments", err_cap-1); + return 1; + } + *out_ptr = NULL; + *out_len = 0; + + lzma_stream strm = LZMA_STREAM_INIT; + lzma_ret ret = lzma_stream_decoder(&strm, UINT64_MAX, LZMA_CONCATENATED); + if (ret != LZMA_OK) { + if (err_buf && err_cap) strncpy(err_buf, "decoder init failed", err_cap-1); + return 2; + } + + strm.next_in = in; + strm.avail_in = in_len; + + size_t cap = in_len * 4; + if (cap < 1024 * 1024) cap = 1024 * 1024; + if (cap > (2048ULL * 1024ULL * 1024ULL)) cap = (2048ULL * 1024ULL * 1024ULL); + + uint8_t *out = (uint8_t*) malloc(cap); + if (!out) { + if (err_buf && err_cap) strncpy(err_buf, "malloc failed", err_cap-1); + lzma_end(&strm); + return 3; + } + + size_t written = 0; + + while (1) { + strm.next_out = out + written; + strm.avail_out = (cap - written); + + if (strm.avail_out == 0) { + size_t new_cap = cap * 2; + if (new_cap > (2048ULL * 1024ULL * 1024ULL)) { + new_cap = (2048ULL * 1024ULL * 1024ULL); + } + if (new_cap <= cap) { + if (err_buf && err_cap) strncpy(err_buf, "exceeded max output size", err_cap-1); + free(out); + lzma_end(&strm); + return 4; + } + uint8_t *tmp = (uint8_t*) realloc(out, new_cap); + if (!tmp) { + if (err_buf && err_cap) strncpy(err_buf, "realloc failed", err_cap-1); + free(out); + lzma_end(&strm); + return 5; + } + out = tmp; + cap = new_cap; + continue; + } + + ret = lzma_code(&strm, LZMA_FINISH); + if (ret == LZMA_STREAM_END) { + written = cap - strm.avail_out; + break; + } else if (ret != LZMA_OK) { + if (err_buf && err_cap) { + switch (ret) { + case LZMA_MEM_ERROR: strncpy(err_buf, "mem error", err_cap-1); break; + case LZMA_MEMLIMIT_ERROR: strncpy(err_buf, "memlimit", err_cap-1); break; + case LZMA_FORMAT_ERROR: strncpy(err_buf, "format error", err_cap-1); break; + case LZMA_OPTIONS_ERROR: strncpy(err_buf, "options error", err_cap-1); break; + case LZMA_DATA_ERROR: strncpy(err_buf, "data error", err_cap-1); break; + default: strncpy(err_buf, "decode error", err_cap-1); break; + } + } + free(out); + lzma_end(&strm); + return 6; + } + + written = cap - strm.avail_out; + } + + lzma_end(&strm); + *out_ptr = out; + *out_len = written; + return 0; +} From 22a340f43f44ba9c8525b71e2478084a2016bab3 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Wed, 19 Nov 2025 17:33:28 -0600 Subject: [PATCH 007/145] Moving wasm to correct dir --- .github/workflows/deploy.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f4211d3..f47f911 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,8 +46,9 @@ jobs: cp styles.css _site/ cp liblzma-streaming-worker.js _site/ # Copy built WASM files - cp liblzma-wasm/dist-streaming/liblzma-xz-streaming.js _site/ - cp liblzma-wasm/dist-streaming/liblzma-xz-streaming.wasm _site/ + mkdir -p _site/liblzma-wasm/dist-streaming/ + cp liblzma-wasm/dist-streaming/liblzma-xz-streaming.js _site/liblzma-wasm/dist-streaming/ + cp liblzma-wasm/dist-streaming/liblzma-xz-streaming.wasm _site/liblzma-wasm/dist-streaming/ cp README.md _site/ || true - name: List deployment files From 23a456d402a7d3337b9876c4137ebdb0545d9903 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Wed, 19 Nov 2025 17:54:51 -0600 Subject: [PATCH 008/145] properties of undefined, error in github pages --- rca-tool/liblzma-streaming-worker.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/rca-tool/liblzma-streaming-worker.js b/rca-tool/liblzma-streaming-worker.js index 5d97daf..3133671 100644 --- a/rca-tool/liblzma-streaming-worker.js +++ b/rca-tool/liblzma-streaming-worker.js @@ -2468,11 +2468,32 @@ LZMA_XZ_Streaming_Module({ } }).then((mod) => { Module = mod; + debugLog('[XZ Streaming Worker] Module object received'); + + // Verify critical properties are available + if (!Module.HEAPU8) { + console.error('[XZ Streaming Worker] HEAPU8 not available in module'); + console.error('[XZ Streaming Worker] Available properties:', Object.keys(Module)); + self.postMessage({ error: 'WASM module initialization incomplete: HEAPU8 missing' }); + moduleReady = false; + return; + } + if (!Module._xz_stream_init || !Module._xz_stream_process) { + console.error('[XZ Streaming Worker] Required functions not available'); + console.error('[XZ Streaming Worker] Available functions:', Object.keys(Module).filter(k => k.startsWith('_'))); + self.postMessage({ error: 'WASM module initialization incomplete: functions missing' }); + moduleReady = false; + return; + } + moduleReady = true; debugLog('[XZ Streaming Worker] Module initialized successfully'); + debugLog('[XZ Streaming Worker] HEAPU8 available:', !!Module.HEAPU8); debugLog('[XZ Streaming Worker] Exported functions:', Object.keys(Module).filter(k => k.startsWith('_'))); + // Signal to main thread that worker is ready self.postMessage({ ready: true }); + debugLog('[XZ Streaming Worker] Ready message sent to main thread'); }).catch((err) => { console.error('[XZ Streaming Worker] Module initialization failed:', err); self.postMessage({ error: 'WASM module initialization failed: ' + err.message }); @@ -3022,8 +3043,8 @@ self.onmessage = async function(e) { // Handle streaming XZ decompression (existing code) if (e.data.cmd === 'decompress_streaming') { - if (!moduleReady) { - self.postMessage({ error: 'Module not ready' }); + if (!moduleReady || !Module || !Module.HEAPU8) { + self.postMessage({ error: 'WASM module not properly initialized' }); return; } From 1cf3d757c75f3755bfeeb85f7ecd8dcd10c7a626 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Wed, 19 Nov 2025 18:00:01 -0600 Subject: [PATCH 009/145] properties of undefined, error in github pages, adding build --- rca-tool/liblzma-wasm/build-liblzma-streaming.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rca-tool/liblzma-wasm/build-liblzma-streaming.sh b/rca-tool/liblzma-wasm/build-liblzma-streaming.sh index 0650e07..184513d 100755 --- a/rca-tool/liblzma-wasm/build-liblzma-streaming.sh +++ b/rca-tool/liblzma-wasm/build-liblzma-streaming.sh @@ -48,7 +48,7 @@ emcc \ -s MODULARIZE=1 \ -s EXPORT_NAME='LZMA_XZ_Streaming_Module' \ -s EXPORTED_FUNCTIONS='["_malloc","_free","_xz_stream_init","_xz_stream_process","_xz_stream_error","_xz_stream_free","_xz_decompress"]' \ - -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","getValue","setValue","UTF8ToString"]' \ + -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","getValue","setValue","UTF8ToString","HEAPU8"]' \ -s ALLOW_MEMORY_GROWTH=1 \ -s INITIAL_MEMORY=16MB \ -s MAXIMUM_MEMORY=2GB \ From 055c39e71b9d784ac661c9a3d9f48675c9497fd1 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Fri, 21 Nov 2025 13:44:34 -0600 Subject: [PATCH 010/145] Adding testing with playwright and MS Edge --- .github/workflows/test.yml | 52 +++++++ rca-tool/tests/analysis.spec.js | 181 ++++++++++++++++++++++ rca-tool/tests/create-fixtures.sh | 232 ++++++++++++++++++++++++++++ rca-tool/tests/package.json | 16 ++ rca-tool/tests/playwright.config.js | 32 ++++ 5 files changed, 513 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 rca-tool/tests/analysis.spec.js create mode 100755 rca-tool/tests/create-fixtures.sh create mode 100644 rca-tool/tests/package.json create mode 100644 rca-tool/tests/playwright.config.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..aa51cfe --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,52 @@ +name: Run Tests + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Setup Emscripten + uses: mymindstorm/setup-emsdk@v14 + with: + version: 'latest' + + - name: Build WASM module + run: | + cd liblzma-wasm + chmod +x build-liblzma-streaming.sh + ./build-liblzma-streaming.sh + + - name: Install dependencies + run: npm ci + working-directory: tests + + - name: Install Playwright Browsers + run: npx playwright install --with-deps msedge + working-directory: tests + + - name: Run tests + run: npm test + working-directory: tests + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: test-results/ + retention-days: 30 diff --git a/rca-tool/tests/analysis.spec.js b/rca-tool/tests/analysis.spec.js new file mode 100644 index 0000000..76b6542 --- /dev/null +++ b/rca-tool/tests/analysis.spec.js @@ -0,0 +1,181 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +/** + * Helper function to upload a test file and wait for analysis + */ +async function uploadAndWaitForAnalysis(page, filename) { + const filePath = path.join(__dirname, 'fixtures', filename); + + // Upload file + const fileInput = await page.locator('input[type="file"]'); + await fileInput.setInputFiles(filePath); + + // Wait for analysis to complete (look for results content in #output) + await page.waitForFunction(() => { + const output = document.getElementById('output'); + return output && output.innerHTML.length > 100; + }, { timeout: 60000 }); + + // Wait a bit more to ensure all content is rendered + await page.waitForTimeout(2000); + + // Get the analysis result HTML + const resultHTML = await page.locator('#output').innerHTML(); + + return resultHTML; +} + +/** + * Helper to extract text content from result + */ +async function getResultText(page) { + return await page.locator('#output').textContent(); +} + +test.describe('SAP HANA Cluster Analyzer', () => { + + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1')).toContainText('RCA Tool'); + }); + + test('page loads correctly', async ({ page }) => { + await expect(page.locator('#drop-zone')).toBeVisible(); + await expect(page.locator('#drop-zone')).toContainText('Select Cluster File'); + }); + + test('detects systemd false positives are filtered out', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-systemd-messages.tar.xz'); + + // Should NOT contain systemd service messages + expect(result).not.toContain('Getty stopped on'); + expect(result).not.toContain('PatrolAgent stopped on port'); + + // But should still show actual cluster events if present + const resultText = await getResultText(page); + if (resultText.includes('Resource Migration Events')) { + // If there are migration events, verify they're not systemd services + expect(result).not.toMatch(/systemd\[\d+\]:/); + } + }); + + test('detects Azure VM properties correctly', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-azure-vm.tar.xz'); + + expect(result).toContain('Azure VM Properties'); + expect(result).toMatch(/VM ID:|VM Size:|Location:/); + }); + + test('detects cluster nodes and validates /etc/hosts', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-cluster-nodes.tar.xz'); + + expect(result).toContain('Cluster nodes in ha.txt and hosts file'); + // Test should process the hosts file + expect(result).toContain('ha.txt'); + }); + + test('detects Corosync configuration issues', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-corosync-config.tar.xz'); + + expect(result).toContain('Corosync Configuration'); + // Check for token timeout detection + expect(result).toMatch(/token.*\d+/i); + }); + + test('detects Pacemaker resources', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-pacemaker-resources.tar.xz'); + + // The output should contain either resources or a message about them + expect(result).toContain('resource'); + // Should process the ha.txt file + expect(result).toContain('ha.txt'); + }); + + test('detects fencing configuration', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-fencing.tar.xz'); + + // Should process ha.txt and show cluster nodes section + expect(result).toContain('ha.txt'); + expect(result).toContain('Cluster nodes'); + }); + + test('detects kernel reboots', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-kernel-reboots.tar.xz'); + + expect(result).toContain('Kernel Reboots'); + expect(result).toMatch(/\d+ events? found/i); + // Should show reboot timestamps + expect(result).toMatch(/\d{4}-\d{2}-\d{2}|\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}/); + }); + + test('detects OOM killer events', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-oom-killer.tar.xz'); + + if (result.includes('OOM Killer')) { + expect(result).toMatch(/OOM Killer.*\d+ event/i); + // Should show process names + expect(result).toMatch(/invoked oom-killer|Killed process/i); + } + }); + + test('detects antivirus software', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-antivirus.tar.xz'); + + if (result.includes('Antivirus')) { + // Should detect Falcon Sensor or Defender + const hasAntivirus = result.includes('Falcon Sensor') || result.includes('Microsoft Defender'); + expect(hasAntivirus).toBeTruthy(); + + // Should show SAP exclusions status + expect(result).toMatch(/SAP.*exclusion/i); + } + }); + + test('detects live migration events', async ({ page }) => { + const result = await uploadAndWaitForAnalysis(page, 'scc_test-live-migration.tar.xz'); + + if (result.includes('Live Migration Events')) { + expect(result).toMatch(/Hyper-V Live Migration/i); + // Should show migration timestamps + expect(result).toMatch(/\d{4}-\d{2}-\d{2}|\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}/); + } + }); + + test('dark mode toggle works', async ({ page }) => { + // Check initial state (dark mode by default) + const body = page.locator('body'); + await expect(body).toHaveClass(/dark-mode/); + + // Toggle to light mode + await page.click('#theme-toggle'); + await expect(body).not.toHaveClass(/dark-mode/); + + // Toggle back to dark mode + await page.click('#theme-toggle'); + await expect(body).toHaveClass(/dark-mode/); + }); + + test('handles invalid file format gracefully', async ({ page }) => { + const fileInput = await page.locator('input[type="file"]'); + + // Create a dummy text file + const buffer = Buffer.from('This is not a tar file'); + await fileInput.setInputFiles({ + name: 'invalid.txt', + mimeType: 'text/plain', + buffer: buffer, + }); + + // Should show error or reject the file + // Wait for potential error message + await page.waitForTimeout(2000); + + // Result should either be empty or contain error message + const result = await page.locator('#output').textContent(); + const isEmpty = result.trim().length === 0; + const hasError = result.includes('error') || result.includes('invalid'); + + expect(isEmpty || hasError).toBeTruthy(); + }); +}); diff --git a/rca-tool/tests/create-fixtures.sh b/rca-tool/tests/create-fixtures.sh new file mode 100755 index 0000000..ebde5e8 --- /dev/null +++ b/rca-tool/tests/create-fixtures.sh @@ -0,0 +1,232 @@ +#!/bin/bash +# Script to generate test fixtures for SAP HANA Cluster Analyzer + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FIXTURES_DIR="$SCRIPT_DIR/fixtures" + +echo "Creating test fixtures in $FIXTURES_DIR" +mkdir -p "$FIXTURES_DIR" + +# Function to create and compress test data +create_fixture() { + local name=$1 + echo "Creating $name..." + # Create an scc-style directory structure + mkdir -p "scc_$name" + mv test-data/* "scc_$name/" 2>/dev/null || true + rmdir test-data 2>/dev/null || true + tar -cJf "$FIXTURES_DIR/scc_$name.tar.xz" "scc_$name" + rm -rf "scc_$name" + echo "✓ Created scc_$name" +} + +# 1. Test for systemd false positives +echo "" +echo "=== Creating test-systemd-messages.tar.xz ===" +mkdir -p test-data/var/log +cat > test-data/var/log/messages.txt << 'EOF' +2025-11-13T07:39:23.000000+01:00 node01 kernel: Linux version 5.14.0 +2025-11-13T07:39:23.997956+01:00 node01 systemd[1]: Stopping Getty on tty1... +2025-11-13T07:39:24.145728+01:00 node01 S50PatrolAgent.sh[1186580]: [LOG]Stopping PatrolAgent on port 3181. +2025-11-13T07:39:24.350856+01:00 node01 systemd[1]: Stopped Getty on tty1. +2025-11-13T07:39:25.000000+01:00 node01 systemd[1]: Started Session 123 of user root. +EOF +create_fixture "test-systemd-messages" + +# 2. Test for Azure VM properties +echo "" +echo "=== Creating test-azure-vm.tar.xz ===" +mkdir -p test-data +cat > test-data/instance_metadata.json << 'EOF' +{ + "vmSize": "Standard_E4s_v3", + "offer": "sles-sap-15-sp4-byos", + "licenseType": "SLES_BYOS" +} +EOF +mkdir -p test-data/etc +cat > test-data/etc/os-release << 'EOF' +NAME="SUSE Linux Enterprise Server" +VERSION="15-SP4" +ID="sles" +ID_LIKE="suse" +VERSION_ID="15.4" +PRETTY_NAME="SUSE Linux Enterprise Server 15 SP4" +EOF +create_fixture "test-azure-vm" + +# 3. Test for cluster nodes and /etc/hosts validation +echo "" +echo "=== Creating test-cluster-nodes.tar.xz ===" +mkdir -p test-data/etc +cat > test-data/etc/hosts << 'EOF' +127.0.0.1 localhost +10.0.1.10 node1 +10.0.1.11 node2 +EOF +mkdir -p test-data +cat > test-data/ha.txt << 'EOF' +# /etc/hosts +127.0.0.1 localhost +10.0.1.10 node1 +10.0.1.11 node2 +10.0.1.12 node3 +#==[ Configuration File ]==== +EOF +cat > test-data/corosync.conf << 'EOF' +totem { + version: 2 + cluster_name: hana-cluster + token: 30000 +} +nodelist { + node { + ring0_addr: node1 + nodeid: 1 + } + node { + ring0_addr: node2 + nodeid: 2 + } + node { + ring0_addr: node3 + nodeid: 3 + } +} +quorum { + provider: corosync_votequorum + expected_votes: 3 +} +EOF +create_fixture "test-cluster-nodes" + +# 4. Test for Corosync configuration +echo "" +echo "=== Creating test-corosync-config.tar.xz ===" +mkdir -p test-data +cat > test-data/corosync.conf << 'EOF' +totem { + version: 2 + cluster_name: hana-cluster + token: 30000 + consensus: 36000 +} +quorum { + provider: corosync_votequorum + expected_votes: 2 + two_node: 1 +} +EOF +create_fixture "test-corosync-config" + +# 5. Test for Pacemaker resources +echo "" +echo "=== Creating test-pacemaker-resources.tar.xz ===" +mkdir -p test-data +cat > test-data/ha.txt << 'EOF' +#==[ Command ]====# +# /usr/sbin/crm_mon -1 -r -f +Stack: corosync +Current DC: node1 +Last updated: Wed Nov 13 08:00:00 2025 +2 nodes configured +3 resources configured + +Online: [ node1 node2 ] + +Full list of resources: + + rsc_SAPHana_HDB_HDB00 (ocf::heartbeat:SAPHana): Started node1 + rsc_ip_HDB_HDB00 (ocf::heartbeat:IPaddr2): Started node1 + stonith-sbd (stonith:external/sbd): Started node2 +EOF +create_fixture "test-pacemaker-resources" + +# 6. Test for fencing configuration +echo "" +echo "=== Creating test-fencing.tar.xz ===" +mkdir -p test-data +cat > test-data/ha.txt << 'EOF' +#==[ Command ]====# +# /usr/sbin/crm configure show +property cib-bootstrap-options: \ + stonith-enabled=true \ + stonith-timeout=150s + +primitive stonith-fence_azure_arm stonith:fence_azure_arm \ + op monitor interval=3600 timeout=120 +EOF +create_fixture "test-fencing" + +# 7. Test for kernel reboots +echo "" +echo "=== Creating test-kernel-reboots.tar.xz ===" +mkdir -p test-data +cat > test-data/messages << 'EOF' +2025-11-12T23:58:59.000000+01:00 node1 systemd[1]: Shutting down. +2025-11-13T00:00:15.000000+01:00 node1 kernel: Linux version 5.14.0-284.30.1.el9_2.x86_64 (mockbuild@x86-64) (gcc version 11.3.0) #1 SMP PREEMPT_DYNAMIC +2025-11-13T00:00:15.000000+01:00 node1 kernel: Command line: BOOT_IMAGE=(hd0,gpt2)/boot/vmlinuz-5.14.0-284.30.1.el9_2.x86_64 root=/dev/mapper/vg_root-lv_root +2025-11-14T15:30:45.000000+01:00 node1 systemd[1]: Shutting down. +2025-11-14T15:31:00.000000+01:00 node1 kernel: Linux version 5.14.0-284.30.1.el9_2.x86_64 (mockbuild@x86-64) (gcc version 11.3.0) #1 SMP PREEMPT_DYNAMIC +EOF +create_fixture "test-kernel-reboots" + +# 8. Test for OOM killer events +echo "" +echo "=== Creating test-oom-killer.tar.xz ===" +mkdir -p test-data +cat > test-data/messages << 'EOF' +Nov 13 14:23:45 node1 kernel: hdbindexserver invoked oom-killer: gfp_mask=0x201da, order=0, oom_score_adj=0 +Nov 13 14:23:45 node1 kernel: Out of memory: Killed process 12345 (hdbindexserver) total-vm:32768000kB +Nov 13 14:23:46 node1 kernel: oom_reaper: reaped process 12345 (hdbindexserver) +EOF +create_fixture "test-oom-killer" + +# 9. Test for antivirus detection +echo "" +echo "=== Creating test-antivirus.tar.xz ===" +mkdir -p test-data +cat > test-data/rpm.txt << 'EOF' +falcon-sensor-7.10.16203.0-1.el8.x86_64 +gpg-pubkey-7f4f6f89-5e9a18de +EOF +cat > test-data/ps.txt << 'EOF' +root 1234 0.0 0.1 123456 7890 ? Ssl 10:00 0:00 /opt/CrowdStrike/falcon-sensor +EOF +mkdir -p test-data/opt/CrowdStrike +cat > test-data/opt/CrowdStrike/falconctl << 'EOF' +#!/bin/bash +echo "CrowdStrike Falcon Sensor Control" +echo "Version: 7.10.0" +echo "exclusions: /usr/sap,/hana/shared,/hana/data" +EOF +chmod +x test-data/opt/CrowdStrike/falconctl +create_fixture "test-antivirus" + +# 10. Test for live migration events +echo "" +echo "=== Creating test-live-migration.tar.xz ===" +mkdir -p test-data +cat > test-data/messages << 'EOF' +2025-11-13T12:15:30.000000+01:00 node1 kernel: hv_vmbus: Vmbus version:4.0 +2025-11-13T12:15:35.000000+01:00 node1 kernel: hv_utils: Heartbeat IC version 3.0 +2025-11-13T12:15:36.000000+01:00 node1 kernel: hv_balloon: Using dynamic memory protocol version 2.0 +2025-11-13T12:15:37.000000+01:00 node1 kernel: hv_netvsc: ring size: 2MB +2025-11-13T13:45:10.000000+01:00 node1 kernel: hv_utils: Heartbeat IC version 3.0 +2025-11-13T13:45:11.000000+01:00 node1 kernel: hv_balloon: Max. dynamic memory size: 4096 MB +2025-11-13T13:45:12.000000+01:00 node1 kernel: hv_netvsc: opened device VF +EOF +create_fixture "test-live-migration" + +echo "" +echo "=========================================" +echo "✓ All test fixtures created successfully!" +echo "=========================================" +echo "" +echo "Fixtures created in: $FIXTURES_DIR" +echo "" +echo "To run tests:" +echo " npm install" +echo " npm test" diff --git a/rca-tool/tests/package.json b/rca-tool/tests/package.json new file mode 100644 index 0000000..a5e39ae --- /dev/null +++ b/rca-tool/tests/package.json @@ -0,0 +1,16 @@ +{ + "name": "rca-tool-tests", + "version": "1.0.0", + "description": "Tests for RCA Tool", + "scripts": { + "test": "playwright test", + "test:debug": "playwright test --debug", + "test:ui": "playwright test --ui", + "test:headed": "playwright test --headed", + "serve": "http-server -p 8080" + }, + "devDependencies": { + "@playwright/test": "^1.48.0", + "http-server": "^14.1.1" + } +} diff --git a/rca-tool/tests/playwright.config.js b/rca-tool/tests/playwright.config.js new file mode 100644 index 0000000..e671a7d --- /dev/null +++ b/rca-tool/tests/playwright.config.js @@ -0,0 +1,32 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html', { outputFolder: 'test-results' }], + ['junit', { outputFile: 'test-results/junit.xml' }], + ['list'] + ], + use: { + baseURL: 'http://localhost:8080', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + projects: [ + { + name: 'msedge', + use: { ...devices['Desktop Edge'] }, + }, + ], + + webServer: { + command: 'npx http-server ../ -p 8080', + port: 8080, + reuseExistingServer: !process.env.CI, + }, +}); From 5a20578310851c4f3e495bed4e27685a4cea677f Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Fri, 21 Nov 2025 13:55:58 -0600 Subject: [PATCH 011/145] Running tests on all branches --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa51cfe..95dd1e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,7 @@ name: Run Tests on: push: - branches: [ main, master ] pull_request: - branches: [ main, master ] workflow_dispatch: jobs: From c78c9b1771ddfbe11896d73806b333f90392ae77 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Fri, 21 Nov 2025 13:59:38 -0600 Subject: [PATCH 012/145] Fixing paths for tests --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 95dd1e0..b6c8f8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,26 +25,26 @@ jobs: - name: Build WASM module run: | - cd liblzma-wasm + cd rca-too/liblzma-wasm chmod +x build-liblzma-streaming.sh ./build-liblzma-streaming.sh - name: Install dependencies run: npm ci - working-directory: tests + working-directory: rca-tool/tests - name: Install Playwright Browsers run: npx playwright install --with-deps msedge - working-directory: tests + working-directory: rca-tool/tests - name: Run tests run: npm test - working-directory: tests + working-directory: rca-tool/tests - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: test-results - path: test-results/ + path: rca-tool/test-results/ retention-days: 30 From cd4bc5dc51ef76e75df526ad6c7baf046783f100 Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Fri, 21 Nov 2025 14:07:21 -0600 Subject: [PATCH 013/145] Fixing paths for tests, typo --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6c8f8b..1921b95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Build WASM module run: | - cd rca-too/liblzma-wasm + cd rca-tool/liblzma-wasm chmod +x build-liblzma-streaming.sh ./build-liblzma-streaming.sh From ceb59946716686293f1be4045cfeb1dd705c1baf Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Fri, 21 Nov 2025 14:10:53 -0600 Subject: [PATCH 014/145] Adding missing lock file --- rca-tool/tests/package-lock.json | 2230 ++++++++++++++++++++++++++++++ 1 file changed, 2230 insertions(+) create mode 100644 rca-tool/tests/package-lock.json diff --git a/rca-tool/tests/package-lock.json b/rca-tool/tests/package-lock.json new file mode 100644 index 0000000..fcad9f1 --- /dev/null +++ b/rca-tool/tests/package-lock.json @@ -0,0 +1,2230 @@ +{ + "name": "rca-tool-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rca-tool-tests", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.48.0", + "http-server": "^14.1.1", + "nyc": "^17.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", + "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001756", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.259", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz", + "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/nyc": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", + "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^3.3.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^6.0.2", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} From 1a2774733569e282fed9a24cc825e7890aa1ba6b Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Fri, 21 Nov 2025 14:25:39 -0600 Subject: [PATCH 015/145] Adding edge channel for browser --- rca-tool/tests/playwright.config.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rca-tool/tests/playwright.config.js b/rca-tool/tests/playwright.config.js index e671a7d..dd55bcb 100644 --- a/rca-tool/tests/playwright.config.js +++ b/rca-tool/tests/playwright.config.js @@ -20,7 +20,10 @@ export default defineConfig({ projects: [ { name: 'msedge', - use: { ...devices['Desktop Edge'] }, + use: { + ...devices['Desktop Edge'], + channel: 'msedge', + }, }, ], From 1c0223de3869b84051dbe1a5a02cacd7715299aa Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Fri, 21 Nov 2025 14:34:18 -0600 Subject: [PATCH 016/145] Building fixtures, adding tests as a dependency before building --- .github/workflows/deploy.yml | 4 ++++ .github/workflows/test.yml | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f47f911..ec6b760 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,7 +15,11 @@ concurrency: cancel-in-progress: false jobs: + tests: + uses: ./.github/workflows/test.yml + build: + needs: tests runs-on: ubuntu-latest steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1921b95..98c33ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,9 @@ name: Run Tests on: - push: pull_request: workflow_dispatch: + workflow_call: jobs: test: @@ -29,10 +29,16 @@ jobs: chmod +x build-liblzma-streaming.sh ./build-liblzma-streaming.sh - - name: Install dependencies + - name: Install dependencies, Playwright and running tests run: npm ci working-directory: rca-tool/tests - + + - name: Building fixtures + run: | + chmod +x create-fixtures.sh + ./create-fixtures.sh + working-directory: rca-tool/tests + - name: Install Playwright Browsers run: npx playwright install --with-deps msedge working-directory: rca-tool/tests From 1292a8484ffae6feb691448942721eabc67663cc Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Fri, 21 Nov 2025 14:41:11 -0600 Subject: [PATCH 017/145] Fixing results path --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98c33ca..7a2ae05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,5 +52,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: test-results - path: rca-tool/test-results/ + path: rca-tool/tests/test-results/ retention-days: 30 From fba18b6caa9fb9bc83521a4aef510e0bc8e3a77c Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Fri, 21 Nov 2025 15:15:09 -0600 Subject: [PATCH 018/145] Always using streaming, to fix failing tests --- rca-tool/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rca-tool/index.html b/rca-tool/index.html index fa7bda1..12f73cf 100644 --- a/rca-tool/index.html +++ b/rca-tool/index.html @@ -566,7 +566,7 @@

Gzip Decompression Failed

// Attempt streaming decompression for all XZ files (memory-efficient) // Use streaming mode for any compressed XZ file, regardless of size - const STREAMING_THRESHOLD = 1024; // 1KB - essentially always use streaming + const STREAMING_THRESHOLD = 0; // always use streaming let streamingResult = null; if (workerSupported && compressedData.length > STREAMING_THRESHOLD) { @@ -1802,4 +1802,4 @@
File Types:
run(); - \ No newline at end of file + From 8cd0c380c6d55ceafd540907fbe1bb0f23bdcd2d Mon Sep 17 00:00:00 2001 From: Alvaro Figueroa Date: Mon, 24 Nov 2025 09:58:03 -0600 Subject: [PATCH 019/145] Adding accessibility testing, and IBM Colorblind Safe Palette --- .github/workflows/test.yml | 4 + .gitignore | 8 + rca-tool/.gitignore | 7 - rca-tool/index.html | 58 +- rca-tool/styles.css | 55 +- rca-tool/tests/accessibility-check.js | 93 ++ rca-tool/tests/package-lock.json | 1681 +------------------------ rca-tool/tests/package.json | 1 + 8 files changed, 265 insertions(+), 1642 deletions(-) delete mode 100644 rca-tool/.gitignore create mode 100644 rca-tool/tests/accessibility-check.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a2ae05..5632ab3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,6 +47,10 @@ jobs: run: npm test working-directory: rca-tool/tests + - name: Run accessibility checks + run: node accessibility-check.js + working-directory: rca-tool/tests + - name: Upload test results if: always() uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index e81a679..142c245 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,11 @@ *.dll *.exe *.pyc +*.swp + +# rca-tool +test-results/ +playwright-report/ +rca-tool/tests/fixtures/ +rca-tool/tmp/ +node_modules/ diff --git a/rca-tool/.gitignore b/rca-tool/.gitignore deleted file mode 100644 index 63c96fd..0000000 --- a/rca-tool/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Test results and artifacts -test-results/ -playwright-report/ -coverage/ - -# Playwright -playwright/.cache/ diff --git a/rca-tool/index.html b/rca-tool/index.html index 12f73cf..aaf05ad 100644 --- a/rca-tool/index.html +++ b/rca-tool/index.html @@ -1,5 +1,5 @@ - + RCA Tool - WASM @@ -32,37 +32,61 @@

Instructions:

Select Cluster File

Choose a .tar.gz or .tar.xz cluster file (hb_report, crm_report, supportconfig, or sosreport)

Supports both gzip and XZ compression

- +

     
 
     
-
-
diff --git a/rca-tool/web/liblzma-wasm/build-liblzma-streaming.sh b/rca-tool/web/liblzma-wasm/build-liblzma-streaming.sh
deleted file mode 100755
index 6ce3b9a..0000000
--- a/rca-tool/web/liblzma-wasm/build-liblzma-streaming.sh
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/bin/bash
-set -e
-
-echo "=== Building Streaming liblzma WASM ==="
-
-# Directories
-SRC_DIR="xz-5.8.1"
-BUILD_DIR="build-streaming"
-DIST_DIR="dist-streaming"
-
-# Clean previous build
-rm -rf "$BUILD_DIR" "$DIST_DIR"
-mkdir -p "$BUILD_DIR" "$DIST_DIR"
-
-# Download and extract XZ Utils if needed
-if [ ! -d "$SRC_DIR" ]; then
-    echo "Downloading XZ Utils 5.8.1..."
-    wget -q https://github.com/tukaani-project/xz/releases/download/v5.8.1/xz-5.8.1.tar.xz
-    tar -xJf xz-5.8.1.tar.xz
-    rm xz-5.8.1.tar.xz
-fi
-
-# Configure and build liblzma
-echo "Configuring liblzma with emscripten..."
-cd "$SRC_DIR"
-emconfigure ./configure \
-    --host=wasm32-unknown-emscripten \
-    --disable-shared \
-    --enable-static \
-    --disable-xz \
-    --disable-xzdec \
-    --disable-lzmadec \
-    --disable-lzmainfo \
-    --disable-lzma-links \
-    --disable-scripts \
-    --disable-doc \
-    --prefix="$(pwd)/../$BUILD_DIR"
-
-echo "Building liblzma..."
-emmake make -j$(nproc)
-emmake make install
-
-cd ..
-
-echo "Compiling streaming wrapper with emcc..."
-emcc \
-    -O3 \
-    -s WASM=1 \
-    -s MODULARIZE=1 \
-    -s EXPORT_NAME='LZMA_XZ_Streaming_Module' \
-    -s EXPORTED_FUNCTIONS='["_malloc","_free","_xz_stream_init","_xz_stream_process","_xz_stream_error","_xz_stream_free","_xz_decompress"]' \
-    -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","getValue","setValue","UTF8ToString","HEAPU8"]' \
-    -s ALLOW_MEMORY_GROWTH=1 \
-    -s INITIAL_MEMORY=16MB \
-    -s MAXIMUM_MEMORY=2GB \
-    -I"$BUILD_DIR/include" \
-    xz_wrapper_streaming.c \
-    "$BUILD_DIR/lib/liblzma.a" \
-    -o "$DIST_DIR/liblzma-xz-streaming.js"
-
-echo "=== Build Complete ==="
-echo "Output files:"
-ls -lh "$DIST_DIR/"
-echo ""
-echo "Streaming WASM module ready at: $DIST_DIR/liblzma-xz-streaming.{js,wasm}"
diff --git a/rca-tool/web/liblzma-wasm/xz_wrapper_streaming.c b/rca-tool/web/liblzma-wasm/xz_wrapper_streaming.c
deleted file mode 100644
index 51a7439..0000000
--- a/rca-tool/web/liblzma-wasm/xz_wrapper_streaming.c
+++ /dev/null
@@ -1,273 +0,0 @@
-#include 
-#include 
-#include 
-#include 
-
-// Streaming XZ decompressor that processes chunks without loading entire output
-// Maximum chunk size for output (keeps memory bounded)
-#define CHUNK_SIZE (1024 * 1024)  // 1MB chunks
-#define INPUT_BUFFER_SIZE (256 * 1024)  // 256KB input buffer
-
-// Opaque handle for streaming state
-typedef struct {
-    lzma_stream strm;
-    int finished;
-    int error_code;
-    char error_msg[256];
-} xz_stream_state;
-
-// Initialize a streaming XZ decompressor
-// Returns handle (opaque pointer) or NULL on failure
-void* xz_stream_init(char *err_buf, size_t err_cap) {
-    xz_stream_state *state = (xz_stream_state*) calloc(1, sizeof(xz_stream_state));
-    if (!state) {
-        if (err_buf && err_cap) strncpy(err_buf, "state alloc failed", err_cap-1);
-        return NULL;
-    }
-
-    state->strm = (lzma_stream)LZMA_STREAM_INIT;
-    lzma_ret ret = lzma_stream_decoder(&state->strm, UINT64_MAX, LZMA_CONCATENATED);
-    if (ret != LZMA_OK) {
-        if (err_buf && err_cap) strncpy(err_buf, "decoder init failed", err_cap-1);
-        free(state);
-        return NULL;
-    }
-
-    state->finished = 0;
-    state->error_code = 0;
-    state->error_msg[0] = '\0';
-    return (void*)state;
-}
-
-// Process a chunk of compressed input
-// Returns: allocated output buffer (caller must free via free()), or NULL if no output yet or error
-// Sets *out_len to output size (0 if no output)
-// Sets *status: 0=need more input, 1=stream finished, <0=error
-uint8_t* xz_stream_process(void *handle, const uint8_t *in, size_t in_len, size_t *out_len, int *status) {
-    if (!handle || !out_len || !status) {
-        if (status) *status = -1;
-        if (out_len) *out_len = 0;
-        return NULL;
-    }
-
-    xz_stream_state *state = (xz_stream_state*)handle;
-    *out_len = 0;
-    *status = 0;
-
-    if (state->finished) {
-        *status = 1;
-        return NULL;
-    }
-
-    if (state->error_code) {
-        *status = state->error_code;
-        return NULL;
-    }
-
-    // Allocate output buffer for this chunk (2MB to handle larger decompressed chunks)
-    size_t out_capacity = CHUNK_SIZE * 2;
-    uint8_t *out_buf = (uint8_t*) malloc(out_capacity);
-    if (!out_buf) {
-        state->error_code = -2;
-        strncpy(state->error_msg, "output buffer alloc failed", sizeof(state->error_msg)-1);
-        *status = -2;
-        return NULL;
-    }
-
-    // Set input
-    state->strm.next_in = in;
-    state->strm.avail_in = in_len;
-
-    // Set output
-    state->strm.next_out = out_buf;
-    state->strm.avail_out = out_capacity;
-
-    size_t total_output = 0;
-
-    // Keep decoding while we have input OR while the decoder produces output
-    // This handles cases where one compressed chunk expands to multiple output chunks
-    lzma_action action = (in_len == 0) ? LZMA_FINISH : LZMA_RUN;
-    lzma_ret ret;
-    
-    do {
-        size_t out_before = state->strm.avail_out;
-        ret = lzma_code(&state->strm, action);
-        size_t produced = out_before - state->strm.avail_out;
-        total_output += produced;
-
-        if (ret == LZMA_STREAM_END) {
-            state->finished = 1;
-            *out_len = total_output;
-            *status = 1;  // finished
-            if (total_output == 0) {
-                free(out_buf);
-                return NULL;
-            }
-            return out_buf;
-        } else if (ret == LZMA_OK) {
-            // Continue if output buffer is full (more data available)
-            // or if we consumed input but haven't exhausted the internal buffers
-            if (state->strm.avail_out == 0) {
-                // Need to expand output buffer
-                out_capacity *= 2;
-                uint8_t *new_buf = (uint8_t*) realloc(out_buf, out_capacity);
-                if (!new_buf) {
-                    state->error_code = -2;
-                    strncpy(state->error_msg, "output buffer realloc failed", sizeof(state->error_msg)-1);
-                    *status = -2;
-                    free(out_buf);
-                    return NULL;
-                }
-                out_buf = new_buf;
-                state->strm.next_out = out_buf + total_output;
-                state->strm.avail_out = out_capacity - total_output;
-            } else {
-                // No more output available, need more input
-                break;
-            }
-        } else {
-            // Error
-            state->error_code = -(int)ret;
-            // Error
-            state->error_code = -(int)ret;
-            switch (ret) {
-                case LZMA_MEM_ERROR: 
-                    strncpy(state->error_msg, "memory error", sizeof(state->error_msg)-1); 
-                    break;
-                case LZMA_MEMLIMIT_ERROR: 
-                    strncpy(state->error_msg, "memory limit", sizeof(state->error_msg)-1); 
-                    break;
-                case LZMA_FORMAT_ERROR: 
-                    strncpy(state->error_msg, "format error", sizeof(state->error_msg)-1); 
-                    break;
-                case LZMA_OPTIONS_ERROR: 
-                    strncpy(state->error_msg, "options error", sizeof(state->error_msg)-1); 
-                    break;
-                case LZMA_DATA_ERROR: 
-                    strncpy(state->error_msg, "data error", sizeof(state->error_msg)-1); 
-                    break;
-                case LZMA_BUF_ERROR:
-                    strncpy(state->error_msg, "buffer error", sizeof(state->error_msg)-1);
-                    break;
-                default: 
-                    strncpy(state->error_msg, "decode error", sizeof(state->error_msg)-1); 
-                    break;
-            }
-            *status = state->error_code;
-            free(out_buf);
-            return NULL;
-        }
-    } while (ret == LZMA_OK);
-
-    // Return output if any
-    *out_len = total_output;
-    *status = 0;  // need more input
-    if (total_output == 0) {
-        free(out_buf);
-        return NULL;
-    }
-    return out_buf;
-}
-
-// Get error message from state
-const char* xz_stream_error(void *handle) {
-    if (!handle) return "invalid handle";
-    xz_stream_state *state = (xz_stream_state*)handle;
-    return state->error_msg[0] ? state->error_msg : "no error";
-}
-
-// Clean up streaming state
-void xz_stream_free(void *handle) {
-    if (!handle) return;
-    xz_stream_state *state = (xz_stream_state*)handle;
-    lzma_end(&state->strm);
-    free(state);
-}
-
-// Original non-streaming function (kept for backward compatibility)
-int xz_decompress(const uint8_t *in, size_t in_len, uint8_t **out_ptr, size_t *out_len, char *err_buf, size_t err_cap) {
-    if (!in || !out_ptr || !out_len) {
-        if (err_buf && err_cap) strncpy(err_buf, "invalid arguments", err_cap-1);
-        return 1;
-    }
-    *out_ptr = NULL;
-    *out_len = 0;
-
-    lzma_stream strm = LZMA_STREAM_INIT;
-    lzma_ret ret = lzma_stream_decoder(&strm, UINT64_MAX, LZMA_CONCATENATED);
-    if (ret != LZMA_OK) {
-        if (err_buf && err_cap) strncpy(err_buf, "decoder init failed", err_cap-1);
-        return 2;
-    }
-
-    strm.next_in = in;
-    strm.avail_in = in_len;
-
-    size_t cap = in_len * 4;
-    if (cap < 1024 * 1024) cap = 1024 * 1024;
-    if (cap > (2048ULL * 1024ULL * 1024ULL)) cap = (2048ULL * 1024ULL * 1024ULL);
-
-    uint8_t *out = (uint8_t*) malloc(cap);
-    if (!out) {
-        if (err_buf && err_cap) strncpy(err_buf, "malloc failed", err_cap-1);
-        lzma_end(&strm);
-        return 3;
-    }
-
-    size_t written = 0;
-
-    while (1) {
-        strm.next_out = out + written;
-        strm.avail_out = (cap - written);
-
-        if (strm.avail_out == 0) {
-            size_t new_cap = cap * 2;
-            if (new_cap > (2048ULL * 1024ULL * 1024ULL)) {
-                new_cap = (2048ULL * 1024ULL * 1024ULL);
-            }
-            if (new_cap <= cap) {
-                if (err_buf && err_cap) strncpy(err_buf, "exceeded max output size", err_cap-1);
-                free(out);
-                lzma_end(&strm);
-                return 4;
-            }
-            uint8_t *tmp = (uint8_t*) realloc(out, new_cap);
-            if (!tmp) {
-                if (err_buf && err_cap) strncpy(err_buf, "realloc failed", err_cap-1);
-                free(out);
-                lzma_end(&strm);
-                return 5;
-            }
-            out = tmp;
-            cap = new_cap;
-            continue;
-        }
-
-        ret = lzma_code(&strm, LZMA_FINISH);
-        if (ret == LZMA_STREAM_END) {
-            written = cap - strm.avail_out;
-            break;
-        } else if (ret != LZMA_OK) {
-            if (err_buf && err_cap) {
-                switch (ret) {
-                    case LZMA_MEM_ERROR: strncpy(err_buf, "mem error", err_cap-1); break;
-                    case LZMA_MEMLIMIT_ERROR: strncpy(err_buf, "memlimit", err_cap-1); break;
-                    case LZMA_FORMAT_ERROR: strncpy(err_buf, "format error", err_cap-1); break;
-                    case LZMA_OPTIONS_ERROR: strncpy(err_buf, "options error", err_cap-1); break;
-                    case LZMA_DATA_ERROR: strncpy(err_buf, "data error", err_cap-1); break;
-                    default: strncpy(err_buf, "decode error", err_cap-1); break;
-                }
-            }
-            free(out);
-            lzma_end(&strm);
-            return 6;
-        }
-
-        written = cap - strm.avail_out;
-    }
-
-    lzma_end(&strm);
-    *out_ptr = out;
-    *out_len = written;
-    return 0;
-}
diff --git a/rca-tool/web/package-lock.json b/rca-tool/web/package-lock.json
deleted file mode 100644
index 34e4d00..0000000
--- a/rca-tool/web/package-lock.json
+++ /dev/null
@@ -1,1223 +0,0 @@
-{
-  "name": "rca-tool-web",
-  "version": "1.0.0",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "rca-tool-web",
-      "version": "1.0.0",
-      "license": "ISC",
-      "devDependencies": {
-        "terser": "^5.44.1",
-        "vite": "^7.2.7"
-      }
-    },
-    "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
-      "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "aix"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/android-arm": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
-      "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/android-arm64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
-      "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/android-x64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
-      "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
-      "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/darwin-x64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
-      "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
-      "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
-      "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-arm": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
-      "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-arm64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
-      "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-ia32": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
-      "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-loong64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
-      "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
-      "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
-      "cpu": [
-        "mips64el"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
-      "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
-      "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-s390x": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
-      "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-x64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
-      "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/netbsd-arm64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
-      "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
-      "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/openbsd-arm64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
-      "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
-      "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/openharmony-arm64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
-      "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "openharmony"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/sunos-x64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
-      "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/win32-arm64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
-      "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/win32-ia32": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
-      "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/win32-x64": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
-      "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@jridgewell/gen-mapping": {
-      "version": "0.3.13",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
-      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@jridgewell/sourcemap-codec": "^1.5.0",
-        "@jridgewell/trace-mapping": "^0.3.24"
-      }
-    },
-    "node_modules/@jridgewell/resolve-uri": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
-      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/@jridgewell/source-map": {
-      "version": "0.3.11",
-      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
-      "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@jridgewell/gen-mapping": "^0.3.5",
-        "@jridgewell/trace-mapping": "^0.3.25"
-      }
-    },
-    "node_modules/@jridgewell/sourcemap-codec": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
-      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/@jridgewell/trace-mapping": {
-      "version": "0.3.31",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
-      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@jridgewell/resolve-uri": "^3.1.0",
-        "@jridgewell/sourcemap-codec": "^1.4.14"
-      }
-    },
-    "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
-      "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "android"
-      ]
-    },
-    "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
-      "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "android"
-      ]
-    },
-    "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
-      "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ]
-    },
-    "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
-      "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ]
-    },
-    "node_modules/@rollup/rollup-freebsd-arm64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
-      "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "freebsd"
-      ]
-    },
-    "node_modules/@rollup/rollup-freebsd-x64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
-      "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "freebsd"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
-      "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
-      "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
-      "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
-      "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-loong64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
-      "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-loong64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
-      "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
-      "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-ppc64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
-      "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
-      "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-riscv64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
-      "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
-      "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
-      "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
-      "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-openbsd-x64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
-      "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "openbsd"
-      ]
-    },
-    "node_modules/@rollup/rollup-openharmony-arm64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
-      "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "openharmony"
-      ]
-    },
-    "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
-      "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
-      "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/@rollup/rollup-win32-x64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
-      "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
-      "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/@types/estree": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
-      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/acorn": {
-      "version": "8.15.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
-      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
-      "dev": true,
-      "license": "MIT",
-      "bin": {
-        "acorn": "bin/acorn"
-      },
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
-    "node_modules/buffer-from": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
-      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/commander": {
-      "version": "2.20.3",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/esbuild": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
-      "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
-      "dev": true,
-      "hasInstallScript": true,
-      "license": "MIT",
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=18"
-      },
-      "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.27.2",
-        "@esbuild/android-arm": "0.27.2",
-        "@esbuild/android-arm64": "0.27.2",
-        "@esbuild/android-x64": "0.27.2",
-        "@esbuild/darwin-arm64": "0.27.2",
-        "@esbuild/darwin-x64": "0.27.2",
-        "@esbuild/freebsd-arm64": "0.27.2",
-        "@esbuild/freebsd-x64": "0.27.2",
-        "@esbuild/linux-arm": "0.27.2",
-        "@esbuild/linux-arm64": "0.27.2",
-        "@esbuild/linux-ia32": "0.27.2",
-        "@esbuild/linux-loong64": "0.27.2",
-        "@esbuild/linux-mips64el": "0.27.2",
-        "@esbuild/linux-ppc64": "0.27.2",
-        "@esbuild/linux-riscv64": "0.27.2",
-        "@esbuild/linux-s390x": "0.27.2",
-        "@esbuild/linux-x64": "0.27.2",
-        "@esbuild/netbsd-arm64": "0.27.2",
-        "@esbuild/netbsd-x64": "0.27.2",
-        "@esbuild/openbsd-arm64": "0.27.2",
-        "@esbuild/openbsd-x64": "0.27.2",
-        "@esbuild/openharmony-arm64": "0.27.2",
-        "@esbuild/sunos-x64": "0.27.2",
-        "@esbuild/win32-arm64": "0.27.2",
-        "@esbuild/win32-ia32": "0.27.2",
-        "@esbuild/win32-x64": "0.27.2"
-      }
-    },
-    "node_modules/fdir": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
-      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=12.0.0"
-      },
-      "peerDependencies": {
-        "picomatch": "^3 || ^4"
-      },
-      "peerDependenciesMeta": {
-        "picomatch": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/fsevents": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
-      "hasInstallScript": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-      }
-    },
-    "node_modules/nanoid": {
-      "version": "3.3.11",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
-      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "license": "MIT",
-      "bin": {
-        "nanoid": "bin/nanoid.cjs"
-      },
-      "engines": {
-        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
-      }
-    },
-    "node_modules/picocolors": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
-      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
-      "dev": true,
-      "license": "ISC"
-    },
-    "node_modules/picomatch": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
-      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
-    "node_modules/postcss": {
-      "version": "8.5.6",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
-      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/postcss/"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/postcss"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "nanoid": "^3.3.11",
-        "picocolors": "^1.1.1",
-        "source-map-js": "^1.2.1"
-      },
-      "engines": {
-        "node": "^10 || ^12 || >=14"
-      }
-    },
-    "node_modules/rollup": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
-      "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@types/estree": "1.0.8"
-      },
-      "bin": {
-        "rollup": "dist/bin/rollup"
-      },
-      "engines": {
-        "node": ">=18.0.0",
-        "npm": ">=8.0.0"
-      },
-      "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.55.1",
-        "@rollup/rollup-android-arm64": "4.55.1",
-        "@rollup/rollup-darwin-arm64": "4.55.1",
-        "@rollup/rollup-darwin-x64": "4.55.1",
-        "@rollup/rollup-freebsd-arm64": "4.55.1",
-        "@rollup/rollup-freebsd-x64": "4.55.1",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
-        "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
-        "@rollup/rollup-linux-arm64-gnu": "4.55.1",
-        "@rollup/rollup-linux-arm64-musl": "4.55.1",
-        "@rollup/rollup-linux-loong64-gnu": "4.55.1",
-        "@rollup/rollup-linux-loong64-musl": "4.55.1",
-        "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
-        "@rollup/rollup-linux-ppc64-musl": "4.55.1",
-        "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
-        "@rollup/rollup-linux-riscv64-musl": "4.55.1",
-        "@rollup/rollup-linux-s390x-gnu": "4.55.1",
-        "@rollup/rollup-linux-x64-gnu": "4.55.1",
-        "@rollup/rollup-linux-x64-musl": "4.55.1",
-        "@rollup/rollup-openbsd-x64": "4.55.1",
-        "@rollup/rollup-openharmony-arm64": "4.55.1",
-        "@rollup/rollup-win32-arm64-msvc": "4.55.1",
-        "@rollup/rollup-win32-ia32-msvc": "4.55.1",
-        "@rollup/rollup-win32-x64-gnu": "4.55.1",
-        "@rollup/rollup-win32-x64-msvc": "4.55.1",
-        "fsevents": "~2.3.2"
-      }
-    },
-    "node_modules/source-map": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-      "dev": true,
-      "license": "BSD-3-Clause",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/source-map-js": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
-      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
-      "dev": true,
-      "license": "BSD-3-Clause",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/source-map-support": {
-      "version": "0.5.21",
-      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
-      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "buffer-from": "^1.0.0",
-        "source-map": "^0.6.0"
-      }
-    },
-    "node_modules/terser": {
-      "version": "5.44.1",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
-      "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "@jridgewell/source-map": "^0.3.3",
-        "acorn": "^8.15.0",
-        "commander": "^2.20.0",
-        "source-map-support": "~0.5.20"
-      },
-      "bin": {
-        "terser": "bin/terser"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/tinyglobby": {
-      "version": "0.2.15",
-      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
-      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "fdir": "^6.5.0",
-        "picomatch": "^4.0.3"
-      },
-      "engines": {
-        "node": ">=12.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/SuperchupuDev"
-      }
-    },
-    "node_modules/vite": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
-      "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "esbuild": "^0.27.0",
-        "fdir": "^6.5.0",
-        "picomatch": "^4.0.3",
-        "postcss": "^8.5.6",
-        "rollup": "^4.43.0",
-        "tinyglobby": "^0.2.15"
-      },
-      "bin": {
-        "vite": "bin/vite.js"
-      },
-      "engines": {
-        "node": "^20.19.0 || >=22.12.0"
-      },
-      "funding": {
-        "url": "https://github.com/vitejs/vite?sponsor=1"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.3"
-      },
-      "peerDependencies": {
-        "@types/node": "^20.19.0 || >=22.12.0",
-        "jiti": ">=1.21.0",
-        "less": "^4.0.0",
-        "lightningcss": "^1.21.0",
-        "sass": "^1.70.0",
-        "sass-embedded": "^1.70.0",
-        "stylus": ">=0.54.8",
-        "sugarss": "^5.0.0",
-        "terser": "^5.16.0",
-        "tsx": "^4.8.1",
-        "yaml": "^2.4.2"
-      },
-      "peerDependenciesMeta": {
-        "@types/node": {
-          "optional": true
-        },
-        "jiti": {
-          "optional": true
-        },
-        "less": {
-          "optional": true
-        },
-        "lightningcss": {
-          "optional": true
-        },
-        "sass": {
-          "optional": true
-        },
-        "sass-embedded": {
-          "optional": true
-        },
-        "stylus": {
-          "optional": true
-        },
-        "sugarss": {
-          "optional": true
-        },
-        "terser": {
-          "optional": true
-        },
-        "tsx": {
-          "optional": true
-        },
-        "yaml": {
-          "optional": true
-        }
-      }
-    }
-  }
-}
diff --git a/rca-tool/web/package.json b/rca-tool/web/package.json
deleted file mode 100644
index 18de755..0000000
--- a/rca-tool/web/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-  "name": "rca-tool-web",
-  "version": "1.0.0",
-  "description": "RCA Tool Web Interface - SAP HANA Cluster Analyzer for supportconfig/sosreport analysis",
-  "type": "module",
-  "scripts": {
-    "dev": "vite",
-    "build": "bash build.sh",
-    "build:vite": "vite build",
-    "preview": "vite preview",
-    "prepare": "mkdir -p public && cp -r liblzma-wasm public/ 2>/dev/null || true"
-  },
-  "keywords": [
-    "sap",
-    "hana",
-    "cluster",
-    "analyzer",
-    "supportconfig",
-    "sosreport"
-  ],
-  "author": "",
-  "license": "ISC",
-  "devDependencies": {
-    "terser": "^5.44.1",
-    "vite": "^7.2.7"
-  }
-}
diff --git a/rca-tool/web/styles.css b/rca-tool/web/styles.css
deleted file mode 100644
index 179e782..0000000
--- a/rca-tool/web/styles.css
+++ /dev/null
@@ -1,497 +0,0 @@
-/* CSS Variables for Light and Dark Mode */
-:root {
-    --bg-color: #f0f0f0;
-    --text-color: #333;
-    --card-bg: white;
-    --card-shadow: rgba(0,0,0,0.1);
-    --accent-color: #007bff;
-    --muted-text: #666;
-    --border-color: #ccc;
-    
-    /* Semantic colors */
-    --success-color: #28a745;
-    --danger-color: #dc3545;
-    --warning-color: #ffc107;
-    --info-color: #17a2b8;
-    --warning-text: #856404;
-    --danger-dark: #721c24;
-    --info-dark: #0c5460;
-    
-    /* Code and inline elements */
-    --code-bg: #f8f9fa;
-    --code-border: #e9ecef;
-    --progress-bg: #f0f0f0;
-    
-    /* Drop zone colors */
-    --dropzone-bg: #e3f2fd;
-    --dropzone-border: #007bff;
-    --dropzone-bg-active: #cce5ff;
-}
-
-body.dark-mode {
-    --bg-color: #1a1a1a;
-    --text-color: #e0e0e0;
-    --card-bg: #2d2d2d;
-    --card-shadow: rgba(0,0,0,0.3);
-    --accent-color: #4a9eff;
-    --muted-text: #aaa;
-    --border-color: #444;
-    
-    /* Semantic colors - adjusted for dark mode (WCAG AA contrast with white text) */
-    --success-color: #2e8b57;
-    --danger-color: #c62828;
-    --warning-color: #ffb74d;
-    --info-color: #0277bd;
-    --warning-text: #ffb74d;
-    --danger-dark: #ef5350;
-    --info-dark: #29b6f6;
-    
-    /* Code and inline elements */
-    --code-bg: #3a3a3a;
-    --code-border: #4a4a4a;
-    --progress-bg: #2d2d2d;
-    
-    /* Drop zone colors */
-    --dropzone-bg: #1e3a5f;
-    --dropzone-border: #4a9eff;
-    --dropzone-bg-active: #2a4a7f;
-}
-
-/* Color-Blind Friendly Mode (IBM Color Blind Safe Palette) */
-body.colorblind-mode {
-    --bg-color: #f5f5f5;
-    --text-color: #2d2d2d;
-    --card-bg: white;
-    --card-shadow: rgba(0,0,0,0.1);
-    --accent-color: #648FFF;
-    --muted-text: #666;
-    --border-color: #bbb;
-    
-    /* Semantic colors - optimized for color blindness */
-    --success-color: #648FFF; /* Blue instead of green */
-    --danger-color: #DC267F; /* Magenta instead of red */
-    --warning-color: #FFB000; /* Yellow - highly visible */
-    --info-color: #785EF0; /* Purple for info */
-    --warning-text: #B37800;
-    --danger-dark: #A01B5E;
-    --info-dark: #5940B8;
-    
-    /* Code and inline elements */
-    --code-bg: #f8f9fa;
-    --code-border: #e1e4e8;
-    --progress-bg: #e8e8e8;
-    
-    /* Drop zone colors */
-    --dropzone-bg: #E6EDFF;
-    --dropzone-border: #648FFF;
-    --dropzone-bg-active: #D4E0FF;
-}
-
-/* Base Styles */
-body {
-    font-family: Arial, sans-serif;
-    margin: 0;
-    padding: 20px;
-    background-color: var(--bg-color);
-    color: var(--text-color);
-    transition: background-color 0.3s, color 0.3s;
-}
-
-h1 {
-    text-align: center;
-    color: var(--text-color);
-    margin-bottom: 30px;
-}
-
-.container {
-    max-width: 800px;
-    margin: 0 auto;
-    position: relative;
-}
-
-/* Info Box Styles */
-.info {
-    background: var(--card-bg);
-    padding: 15px;
-    border-radius: 5px;
-    margin-bottom: 20px;
-    box-shadow: 0 2px 5px var(--card-shadow);
-    transition: background-color 0.3s, box-shadow 0.3s;
-}
-
-.info h3 {
-    margin-top: 0;
-    color: var(--accent-color);
-}
-
-.info ul {
-    margin: 10px 0;
-    padding-left: 20px;
-}
-
-/* Loading Indicator */
-#loading {
-    text-align: center;
-    padding: 20px;
-    color: var(--muted-text);
-}
-
-/* Theme Toggle Button */
-#theme-toggle {
-    position: fixed;
-    top: 20px;
-    right: 20px;
-    width: 50px;
-    height: 50px;
-    border-radius: 50%;
-    border: 2px solid var(--border-color);
-    background: var(--card-bg);
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 24px;
-    box-shadow: 0 2px 5px var(--card-shadow);
-    transition: all 0.3s;
-    z-index: 1000;
-}
-
-#theme-toggle:hover {
-    transform: scale(1.1);
-}
-
-#theme-toggle:focus {
-    outline: 2px solid var(--accent-color);
-    outline-offset: 2px;
-}
-
-/* Drop Zone Styles */
-#drop-zone {
-    border: 2px dashed var(--border-color);
-    border-radius: 10px;
-    width: 500px;
-    max-width: 90%;
-    min-height: 200px;
-    text-align: center;
-    padding: 20px;
-    margin: 20px auto;
-    font-family: Arial, sans-serif;
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    align-items: center;
-    background-color: var(--card-bg);
-    transition: background-color 0.3s, border-color 0.3s;
-}
-
-#drop-zone h2 {
-    color: var(--text-color);
-    margin: 0 0 10px 0;
-}
-
-#drop-zone p {
-    color: var(--text-color);
-    margin: 5px 0;
-}
-
-/* File Input Styles */
-#file-input {
-    padding: 12px;
-    margin: 15px;
-    font-size: 16px;
-    cursor: pointer;
-    border: 2px solid var(--accent-color);
-    border-radius: 5px;
-    background: var(--card-bg);
-    color: var(--text-color);
-    transition: all 0.3s;
-}
-
-/* Detection Warning Styles */
-.detection-warning {
-    margin-top: 15px;
-    padding: 12px 15px;
-    font-size: 0.85em;
-    color: var(--warning-text, #856404);
-    background-color: var(--warning-bg, #fff3cd);
-    border: 1px solid var(--warning-border, #ffc107);
-    border-radius: 5px;
-    text-align: left;
-    max-width: 600px;
-    margin-left: auto;
-    margin-right: auto;
-}
-
-/* Content Block Styles - Dark Mode Variables */
-body.dark-mode {
-    --success-bg: #1e4620;
-    --success-text: #a8d5a8;
-    --success-border: #4caf50;
-    --warning-bg: #5a4a1f;
-    --warning-text: #f5d98f;
-    --warning-border: #ffc107;
-    --danger-bg: #4a1f1f;
-    --danger-text: #f5a8a8;
-    --danger-border: #dc3545;
-    --info-bg: #1f3a4a;
-    --info-text: #a8c8d8;
-    --info-border: #17a2b8;
-    --content-bg: #2d2d2d;
-    --content-border: #444;
-}
-
-/* Content Block Styles - Light Mode Variables */
-body:not(.dark-mode):not(.colorblind-mode) {
-    --success-bg: #d4edda;
-    --success-text: #155724;
-    --success-border: #28a745;
-    --warning-bg: #fff3cd;
-    --warning-text: #856404;
-    --warning-border: #ffc107;
-    --danger-bg: #f8d7da;
-    --danger-text: #721c24;
-    --danger-border: #dc3545;
-    --info-bg: #d1ecf1;
-    --info-text: #0c5460;
-    --info-border: #17a2b8;
-    --content-bg: white;
-    --content-border: #dee2e6;
-}
-
-/* Content Block Styles - Color-Blind Mode Variables */
-body.colorblind-mode {
-    --success-bg: #E6EDFF;
-    --success-text: #2D4AA8;
-    --success-border: #648FFF;
-    --warning-bg: #FFF5E0;
-    --warning-text: #B37800;
-    --warning-border: #FFB000;
-    --danger-bg: #FCE4F0;
-    --danger-text: #A01B5E;
-    --danger-border: #DC267F;
-    --info-bg: #F0EBFF;
-    --info-text: #5940B8;
-    --info-border: #785EF0;
-    --content-bg: white;
-    --content-border: #d0d0d0;
-}
-
-/* Success Block */
-.success-block {
-    background: var(--success-bg, #d4edda);
-    color: var(--success-text, #155724);
-    padding: 15px;
-    border-radius: 5px;
-    border-left: 4px solid var(--success-border, #28a745);
-    margin-bottom: 15px;
-}
-
-/* Warning Block */
-.warning-block {
-    background: var(--warning-bg);
-    color: var(--warning-text);
-    padding: 15px;
-    border-radius: 5px;
-    border-left: 4px solid var(--warning-border);
-    margin-bottom: 15px;
-}
-
-/* Danger Block */
-.danger-block {
-    background: var(--danger-bg);
-    color: var(--danger-text);
-    padding: 15px;
-    border-radius: 5px;
-    border-left: 4px solid var(--danger-border);
-    margin-bottom: 15px;
-}
-
-/* Info Block */
-.info-block {
-    background: var(--info-bg);
-    color: var(--info-text);
-    padding: 15px;
-    border-radius: 5px;
-    border-left: 4px solid var(--info-border);
-    margin-bottom: 15px;
-}
-
-/* Content Details */
-.content-details {
-    background: var(--content-bg);
-    padding: 15px;
-    border-radius: 5px;
-    border: 1px solid var(--content-border);
-    margin-bottom: 15px;
-}
-
-.content-details summary {
-    cursor: pointer;
-    font-weight: bold;
-    user-select: none;
-    color: var(--text-color);
-}
-
-/* Utility Classes for Text Colors */
-.text-success { color: var(--success-color); }
-.text-danger { color: var(--danger-color); }
-.text-warning { color: var(--warning-color); }
-.text-info { color: var(--info-color); }
-.text-muted { color: var(--muted-text); }
-.text-warning-dark { color: var(--warning-text); }
-.text-danger-dark { color: var(--danger-dark); }
-.text-info-dark { color: var(--info-dark); }
-
-/* Utility Classes for Backgrounds */
-.bg-code { background: var(--code-bg); }
-.bg-card { background: var(--card-bg); }
-
-/* Code Elements */
-code {
-    background: var(--code-bg);
-    padding: 2px 6px;
-    border-radius: 3px;
-    font-family: monospace;
-    font-size: 0.9em;
-}
-
-/* Badge Styles */
-.badge {
-    padding: 1px 5px;
-    border-radius: 3px;
-    font-size: 11px;
-    margin-left: 5px;
-    display: inline-block;
-}
-
-.badge-success {
-    background: var(--success-color);
-    color: white;
-}
-
-.badge-danger {
-    background: var(--danger-color);
-    color: white;
-}
-
-.badge-warning {
-    background: var(--warning-color);
-    color: #000;
-}
-
-.badge-info {
-    background: var(--info-color);
-    color: white;
-}
-
-/* Progress Bar */
-.progress-container {
-    text-align: center;
-    padding: 10px;
-}
-
-.progress-title {
-    color: var(--accent-color);
-    margin: 5px 0;
-    font-weight: bold;
-}
-
-.progress-bar-wrapper {
-    margin: 10px auto;
-    max-width: 400px;
-}
-
-.progress-bar {
-    width: 100%;
-    background: var(--progress-bg);
-    border-radius: 5px;
-    padding: 2px;
-}
-
-.progress-fill {
-    width: 0%;
-    background: var(--accent-color);
-    height: 12px;
-    border-radius: 3px;
-    transition: width 0.3s;
-}
-
-.progress-text {
-    font-size: 12px;
-    color: var(--muted-text);
-    margin: 5px 0;
-}
-
-/* List Item Styles */
-.event-item {
-    margin: 8px 0;
-    font-size: 13px;
-    border-bottom: 1px solid var(--code-border);
-    padding-bottom: 8px;
-}
-
-.event-timestamp {
-    font-weight: bold;
-    font-size: 12px;
-    margin-bottom: 3px;
-}
-
-.event-details {
-    font-size: 11px;
-    color: var(--muted-text);
-    margin-top: 3px;
-}
-
-.event-raw-line {
-    font-size: 10px;
-    color: var(--muted-text);
-    font-family: monospace;
-    display: block;
-    margin-top: 4px;
-    padding: 4px;
-    background: var(--code-bg);
-    border-radius: 2px;
-    overflow-x: auto;
-    white-space: nowrap;
-}
-
-/* Subsection Styles */
-.subsection {
-    margin-top: 15px;
-    padding: 10px;
-    background: var(--card-bg);
-    border-radius: 3px;
-}
-
-.subsection-danger {
-    margin-top: 15px;
-    padding: 10px;
-    background: var(--card-bg);
-    border-radius: 3px;
-    border-left: 3px solid var(--danger-color);
-}
-
-/* Experimental CSS for text wrapping */
-pre {
-    white-space: pre-wrap;
-    word-break: break-word;
-}
-
-.danger-block p {
-    white-space: normal;
-}
-
-/* Warning Box */
-.warning-box {
-    margin-top: 15px;
-    padding: 12px;
-    background: var(--card-bg);
-    border-radius: 3px;
-    border-left: 3px solid var(--danger-color);
-    max-width: 100%;
-    box-sizing: border-box;
-    overflow-wrap: break-word;
-    word-break: break-word;
-    white-space: normal;
-}
-
diff --git a/rca-tool/web/vite.config.js b/rca-tool/web/vite.config.js
deleted file mode 100644
index c2ab2fb..0000000
--- a/rca-tool/web/vite.config.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { defineConfig } from 'vite';
-
-export default defineConfig({
-  base: './',
-  build: {
-    outDir: 'dist',
-    assetsDir: 'assets',
-    minify: 'terser',
-    sourcemap: true,
-    rollupOptions: {
-      output: {
-        manualChunks: undefined,
-        entryFileNames: 'assets/[name].[hash].js',
-        chunkFileNames: 'assets/[name].[hash].js',
-        assetFileNames: 'assets/[name].[hash].[ext]'
-      }
-    },
-    terserOptions: {
-      compress: {
-        drop_console: false,
-        drop_debugger: true
-      }
-    },
-    // Copy worker and WASM files
-    copyPublicDir: true
-  },
-  publicDir: 'public',
-  worker: {
-    format: 'es',
-    rollupOptions: {
-      output: {
-        entryFileNames: 'assets/[name].[hash].js'
-      }
-    }
-  },
-  server: {
-    port: 8000,
-    open: true
-  }
-});

From 00e2a97afe148727c669c42e422638b02fe3def5 Mon Sep 17 00:00:00 2001
From: Alvaro Figueroa 
Date: Thu, 25 Jun 2026 14:21:27 -0600
Subject: [PATCH 140/145] Changes to CI, and vscode publisher

---
 .github/workflows/deploy.yml               | 16 ++++++++--------
 .github/workflows/rust-python-packages.yml | 12 ++++++------
 .github/workflows/test.yml                 | 12 ++++++------
 rca-tool/vscode-extension/package.json     |  2 +-
 4 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 8a765e9..6b42a2e 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -33,10 +33,10 @@ jobs:
     
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v4
+      uses: actions/checkout@v6
     
     - name: Setup Node.js
-      uses: actions/setup-node@v4
+      uses: actions/setup-node@v5
       with:
         node-version: '22'
 
@@ -57,7 +57,7 @@ jobs:
     - name: Restore sccache files
       id: restore-sccache
       if: ${{ !startsWith(github.ref, 'refs/tags/') }}
-      uses: actions/cache/restore@v4
+      uses: actions/cache/restore@v5
       with:
         path: ${{ github.workspace }}/.sccache
         key: leptos-web-${{ runner.os }}-${{ runner.arch }}-sccache-
@@ -153,7 +153,7 @@ jobs:
 
     - name: Save sccache files
       if: ${{ always() && !startsWith(github.ref, 'refs/tags/') }}
-      uses: actions/cache/save@v4
+      uses: actions/cache/save@v5
       with:
         key: ${{ steps.restore-sccache.outputs.cache-primary-key }}${{ steps.prepare-save-sccache.outputs.SCCACHE_TIMESTAMP }}
         path: ${{ github.workspace }}/.sccache
@@ -180,10 +180,10 @@ jobs:
     
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v4
+      uses: actions/checkout@v6
       
     - name: Setup Node.js
-      uses: actions/setup-node@v4
+      uses: actions/setup-node@v5
       with:
         node-version: '22'
         
@@ -286,7 +286,7 @@ jobs:
       
     - name: Upload post-deployment test results
       if: always()
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v7
       with:
         name: post-deploy-test-results
         path: |
@@ -296,7 +296,7 @@ jobs:
 
     - name: Upload post-deployment coverage report
       if: always()
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v7
       with:
         name: post-deploy-coverage-report
         path: rca-tool/tests/coverage-report*/
diff --git a/.github/workflows/rust-python-packages.yml b/.github/workflows/rust-python-packages.yml
index 9aa5d99..144480d 100644
--- a/.github/workflows/rust-python-packages.yml
+++ b/.github/workflows/rust-python-packages.yml
@@ -76,7 +76,7 @@ jobs:
       - name: Restore sccache files
         id: restore-sccache
         if: ${{ !startsWith(github.ref, 'refs/tags/') && matrix.os != 'windows' }}
-        uses: actions/cache/restore@v4
+        uses: actions/cache/restore@v5
         with:
           path: ${{ github.workspace }}/.sccache
           key: rust-core-${{ runner.os }}-${{ matrix.arch }}-sccache-
@@ -188,7 +188,7 @@ jobs:
 
       - name: Save sccache files
         if: ${{ always() && !startsWith(github.ref, 'refs/tags/') && matrix.os != 'windows' }}
-        uses: actions/cache/save@v4
+        uses: actions/cache/save@v5
         with:
           key: ${{ steps.restore-sccache.outputs.cache-primary-key }}${{ steps.prepare-save-sccache.outputs.SCCACHE_TIMESTAMP }}
           path: ${{ github.workspace }}/.sccache
@@ -302,9 +302,9 @@ jobs:
         uses: actions/checkout@v6
 
       - name: Setup Node.js
-        uses: actions/setup-node@v4
+        uses: actions/setup-node@v5
         with:
-          node-version: '20'
+          node-version: '24'
 
       - name: Download rca_cli binary
         uses: actions/download-artifact@v5
@@ -506,9 +506,9 @@ jobs:
 
     steps:
       - name: Setup Node.js
-        uses: actions/setup-node@v4
+        uses: actions/setup-node@v5
         with:
-          node-version: '20'
+          node-version: '24'
 
       - name: Download VSIX artifacts
         uses: actions/download-artifact@v5
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2465149..08eb1d1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,10 +20,10 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v4
+      uses: actions/checkout@v6
       
     - name: Setup Node.js
-      uses: actions/setup-node@v4
+      uses: actions/setup-node@v5
       with:
         node-version: '22'
 
@@ -46,7 +46,7 @@ jobs:
     - name: Restore sccache files
       id: restore-sccache
       if: ${{ !startsWith(github.ref, 'refs/tags/') }}
-      uses: actions/cache/restore@v4
+      uses: actions/cache/restore@v5
       with:
         path: ${{ github.workspace }}/.sccache
         key: tests-${{ runner.os }}-${{ runner.arch }}-sccache-
@@ -175,7 +175,7 @@ jobs:
       
     - name: Upload test results
       if: always()
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v7
       with:
         name: test-results-${{ matrix.os }}
         path: |
@@ -185,7 +185,7 @@ jobs:
 
     - name: Upload coverage report
       if: always()
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v7
       with:
         name: coverage-report-${{ matrix.os }}
         path: rca-tool/tests/coverage-report*/
@@ -202,7 +202,7 @@ jobs:
 
     - name: Save sccache files
       if: ${{ always() && !startsWith(github.ref, 'refs/tags/') }}
-      uses: actions/cache/save@v4
+      uses: actions/cache/save@v5
       with:
         key: ${{ steps.restore-sccache.outputs.cache-primary-key }}${{ steps.prepare-save-sccache.outputs.SCCACHE_TIMESTAMP }}
         path: ${{ github.workspace }}/.sccache
diff --git a/rca-tool/vscode-extension/package.json b/rca-tool/vscode-extension/package.json
index 2be07a0..086c10a 100644
--- a/rca-tool/vscode-extension/package.json
+++ b/rca-tool/vscode-extension/package.json
@@ -3,7 +3,7 @@
   "displayName": "RCA Tool",
   "description": "Analyze Linux support archives (sosreport / supportconfig) directly from VS Code Chat via the RCA Tool MCP server.",
   "version": "0.1.0",
-  "publisher": "azure-support-scripts",
+  "publisher": "fede2cr",
   "license": "MIT",
   "repository": {
     "type": "git",

From f4b386618b7a73fcd1f1fef767b4461451f84851 Mon Sep 17 00:00:00 2001
From: Alvaro Figueroa 
Date: Fri, 26 Jun 2026 11:57:12 -0600
Subject: [PATCH 141/145] Adding publisher and PAT

---
 .github/workflows/rust-python-packages.yml | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/.github/workflows/rust-python-packages.yml b/.github/workflows/rust-python-packages.yml
index 144480d..2373456 100644
--- a/.github/workflows/rust-python-packages.yml
+++ b/.github/workflows/rust-python-packages.yml
@@ -307,13 +307,13 @@ jobs:
           node-version: '24'
 
       - name: Download rca_cli binary
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v8
         with:
           name: rca_cli-${{ matrix.artifact_suffix }}
           path: dl-rca
 
       - name: Download supportfile_mcp binary
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v8
         with:
           name: supportfile_mcp-${{ matrix.artifact_suffix }}
           path: dl-mcp
@@ -401,7 +401,7 @@ jobs:
 
     steps:
       - name: Download wheel artifacts
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v8
         with:
           pattern: python-wheels-*
           path: dist
@@ -431,7 +431,7 @@ jobs:
       # Download them into per-artifact subdirectories so the platform-specific
       # names are preserved.
       - name: Download binary artifacts
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v8
         with:
           pattern: '{rca_cli,supportfile_mcp}-*'
           path: artifacts
@@ -439,14 +439,14 @@ jobs:
       # Wheels and source crates already carry unique, version-bearing file
       # names, so they can be merged into single flat directories.
       - name: Download wheel artifacts
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v8
         with:
           pattern: python-wheels-*
           path: wheels
           merge-multiple: true
 
       - name: Download crate artifacts
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v8
         with:
           pattern: '{rust-crate,rust-mcp-crate}-*'
           path: crates
@@ -454,7 +454,7 @@ jobs:
 
       # VSIX packages already carry the platform target in their file name.
       - name: Download VSIX artifacts
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v8
         with:
           pattern: vscode-extension-*
           path: vsix
@@ -511,7 +511,7 @@ jobs:
           node-version: '24'
 
       - name: Download VSIX artifacts
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v8
         with:
           pattern: vscode-extension-*
           path: vsix

From 9e68102232e57fad642f5db9726d5f787b6bbb59 Mon Sep 17 00:00:00 2001
From: Alvaro Figueroa 
Date: Fri, 26 Jun 2026 12:35:05 -0600
Subject: [PATCH 142/145] Testing PAT lenght

---
 .github/workflows/rust-python-packages.yml | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/rust-python-packages.yml b/.github/workflows/rust-python-packages.yml
index 2373456..6f5e6a1 100644
--- a/.github/workflows/rust-python-packages.yml
+++ b/.github/workflows/rust-python-packages.yml
@@ -306,6 +306,19 @@ jobs:
         with:
           node-version: '24'
 
+      # When triggered by a `v` tag, override the manifest version so
+      # the packaged VSIX matches the release tag instead of the committed
+      # placeholder version. vsce reads the version from package.json at
+      # package time, so this must run before `vsce package`.
+      - name: Set extension version from tag
+        if: startsWith(github.ref, 'refs/tags/v')
+        working-directory: ${{ env.EXT_DIR }}
+        run: |
+          set -euo pipefail
+          version="${GITHUB_REF_NAME#v}"
+          echo "Setting extension version to $version (from tag $GITHUB_REF_NAME)"
+          npm version "$version" --no-git-tag-version --allow-same-version
+
       - name: Download rca_cli binary
         uses: actions/download-artifact@v8
         with:
@@ -524,8 +537,13 @@ jobs:
         if: env.VSCE_PAT != ''
         run: |
           set -euo pipefail
+          # Sanity-check the secret without leaking it: report length only.
+          # A mismatch here (e.g. trailing newline/whitespace from a bad paste)
+          # is the most common cause of a 401 when the PAT itself is valid.
+          echo "VSCE_PAT length: ${#VSCE_PAT}"
           # vsce can publish all platform-specific packages in one invocation.
-          npx --yes @vscode/vsce publish --no-dependencies $(printf -- '--packagePath %s ' vsix/*.vsix)
+          # Pass the token explicitly with -p rather than relying on env pickup.
+          npx --yes @vscode/vsce publish --no-dependencies -p "$VSCE_PAT" $(printf -- '--packagePath %s ' vsix/*.vsix)
 
       - name: Skip VS Code Marketplace (no VSCE_PAT)
         if: env.VSCE_PAT == ''

From 77e1f97fab4dcc9e6269f8c2fdc1c2a164f5f662 Mon Sep 17 00:00:00 2001
From: Alvaro Figueroa 
Date: Fri, 26 Jun 2026 12:43:55 -0600
Subject: [PATCH 143/145] Removing version tag for gh pages, but still using it
 as trigger

---
 .github/workflows/deploy.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 6b42a2e..413e079 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -22,7 +22,8 @@ jobs:
   build:
     # Local tests (the `tests` job above) run on every push, but the GitHub
     # Pages build/deploy only happens for version releases (v* tags) or a
-    # manual run.
+    # manual run. The tag only gates the deploy — no version is injected into
+    # the built site.
     if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
     needs: tests
     runs-on: ubuntu-latest

From ca12c1038b73a6422e1ead444ac606a5aa2e5558 Mon Sep 17 00:00:00 2001
From: Alvaro Figueroa 
Date: Fri, 26 Jun 2026 13:35:11 -0600
Subject: [PATCH 144/145] Removing version tag for gh pages, but still using it
 as trigger

---
 .github/workflows/rust-python-packages.yml | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/rust-python-packages.yml b/.github/workflows/rust-python-packages.yml
index 6f5e6a1..5788de1 100644
--- a/.github/workflows/rust-python-packages.yml
+++ b/.github/workflows/rust-python-packages.yml
@@ -374,7 +374,10 @@ jobs:
 
   publish-cargo:
     name: Publish Rust crate to crates.io
-    if: startsWith(github.ref, 'refs/tags/')
+    # Only publish on `v` tags. The version-rewrite step below is gated
+    # on the same condition, so a broader `refs/tags/` gate would publish the
+    # committed placeholder version (0.0.0) for non-`v` tags (e.g. rust-parser-v*).
+    if: startsWith(github.ref, 'refs/tags/v')
     needs:
       - build-rust-core
       - build-python-wheel
@@ -405,7 +408,7 @@ jobs:
 
   publish-pypi:
     name: Publish Python wheels to PyPI
-    if: startsWith(github.ref, 'refs/tags/')
+    if: startsWith(github.ref, 'refs/tags/v')
     needs:
       - build-python-wheel
     runs-on: ubuntu-24.04
@@ -428,7 +431,7 @@ jobs:
 
   publish-release:
     name: Attach binaries to GitHub Release
-    if: startsWith(github.ref, 'refs/tags/')
+    if: startsWith(github.ref, 'refs/tags/v')
     needs:
       - build-rust-core
       - build-python-wheel
@@ -506,7 +509,7 @@ jobs:
 
   publish-vsce:
     name: Publish VS Code extension
-    if: startsWith(github.ref, 'refs/tags/')
+    if: startsWith(github.ref, 'refs/tags/v')
     needs:
       - build-vscode-extension
     runs-on: ubuntu-latest

From e2ca34a5545f8be4cb7f9f907aeb7b7ec7776962 Mon Sep 17 00:00:00 2001
From: Alvaro Figueroa 
Date: Wed, 1 Jul 2026 12:52:33 -0600
Subject: [PATCH 145/145] Removing test output files

---
 .../cli/test-output/scc_test-antivirus.txt    | 18 -------------
 rca-tool/cli/test-output/scc_test-asr.txt     | 19 -------------
 .../cli/test-output/scc_test-automation.txt   | 13 ---------
 .../scc_test-azure-network-tuned.txt          | 13 ---------
 .../scc_test-azure-network-warnings.txt       | 13 ---------
 .../scc_test-azure-vm-rhel-byos.txt           | 22 ---------------
 .../scc_test-azure-vm-rhel-payg.txt           | 22 ---------------
 .../scc_test-azure-vm-scc-metadata.txt        | 22 ---------------
 .../scc_test-azure-vm-sles-payg.txt           | 22 ---------------
 .../test-output/scc_test-azure-vm-storage.txt | 27 -------------------
 .../scc_test-azure-vm-ubuntu-byos.txt         | 22 ---------------
 .../scc_test-azure-vm-ubuntu-pro.txt          | 22 ---------------
 .../cli/test-output/scc_test-azure-vm.txt     | 25 -----------------
 rca-tool/cli/test-output/scc_test-btrfs.txt   | 19 -------------
 .../test-output/scc_test-cluster-nodes.txt    | 18 -------------
 .../scc_test-complete-network-tuning.txt      | 13 ---------
 .../test-output/scc_test-corosync-config.txt  | 13 ---------
 .../scc_test-crm-report-sysinfo.txt           | 19 -------------
 .../cli/test-output/scc_test-dlm-service.txt  | 20 --------------
 .../cli/test-output/scc_test-fencing-pcs.txt  | 20 --------------
 rca-tool/cli/test-output/scc_test-fencing.txt | 18 -------------
 rca-tool/cli/test-output/scc_test-fstab.txt   | 13 ---------
 .../test-output/scc_test-huge-pages-none.txt  | 19 -------------
 .../cli/test-output/scc_test-huge-pages.txt   | 21 ---------------
 rca-tool/cli/test-output/scc_test-illumio.txt | 13 ---------
 .../test-output/scc_test-kernel-reboots.txt   | 13 ---------
 .../scc_test-kernel-tuning-warnings.txt       | 13 ---------
 .../test-output/scc_test-kernel-tuning.txt    | 13 ---------
 .../test-output/scc_test-live-migration.txt   | 13 ---------
 rca-tool/cli/test-output/scc_test-lvm.txt     | 19 -------------
 .../cli/test-output/scc_test-nested-gzip.txt  | 13 ---------
 .../cli/test-output/scc_test-oom-killer.txt   | 13 ---------
 .../scc_test-optional-network-tuning.txt      | 13 ---------
 .../scc_test-pacemaker-resources.txt          | 22 ---------------
 rca-tool/cli/test-output/scc_test-raid.txt    | 13 ---------
 .../test-output/scc_test-systemd-messages.txt | 13 ---------
 .../cli/test-output/scc_test-trendmicro.txt   | 13 ---------
 .../scc_test-xfs-duplicate-uuid.txt           | 13 ---------
 .../cli/test-output/scc_test-xfs-errors.txt   | 13 ---------
 .../scc_test-xfs-timestamp-normalization.txt  | 13 ---------
 .../cli/test-output/sosreport-deb-raw.txt     | 18 -------------
 .../cli/test-output/sosreport-rpm-raw.txt     | 18 -------------
 .../cli/test-output/sosreport-yum-raw.txt     | 18 -------------
 43 files changed, 730 deletions(-)
 delete mode 100644 rca-tool/cli/test-output/scc_test-antivirus.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-asr.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-automation.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-network-tuned.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-network-warnings.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-vm-rhel-byos.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-vm-rhel-payg.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-vm-scc-metadata.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-vm-sles-payg.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-vm-storage.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-vm-ubuntu-byos.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-vm-ubuntu-pro.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-azure-vm.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-btrfs.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-cluster-nodes.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-complete-network-tuning.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-corosync-config.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-crm-report-sysinfo.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-dlm-service.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-fencing-pcs.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-fencing.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-fstab.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-huge-pages-none.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-huge-pages.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-illumio.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-kernel-reboots.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-kernel-tuning-warnings.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-kernel-tuning.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-live-migration.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-lvm.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-nested-gzip.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-oom-killer.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-optional-network-tuning.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-pacemaker-resources.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-raid.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-systemd-messages.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-trendmicro.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-xfs-duplicate-uuid.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-xfs-errors.txt
 delete mode 100644 rca-tool/cli/test-output/scc_test-xfs-timestamp-normalization.txt
 delete mode 100644 rca-tool/cli/test-output/sosreport-deb-raw.txt
 delete mode 100644 rca-tool/cli/test-output/sosreport-rpm-raw.txt
 delete mode 100644 rca-tool/cli/test-output/sosreport-yum-raw.txt

diff --git a/rca-tool/cli/test-output/scc_test-antivirus.txt b/rca-tool/cli/test-output/scc_test-antivirus.txt
deleted file mode 100644
index 69ee36f..0000000
--- a/rca-tool/cli/test-output/scc_test-antivirus.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-antivirus.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 3
-Directories: 3
-
-----------------------------------------------------------------------
-DISTRIBUTION
-----------------------------------------------------------------------
-  Packages: 0 installed
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-asr.txt b/rca-tool/cli/test-output/scc_test-asr.txt
deleted file mode 100644
index 6d9397c..0000000
--- a/rca-tool/cli/test-output/scc_test-asr.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-asr.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 3
-Directories: 1
-
-----------------------------------------------------------------------
-AZURE SITE RECOVERY
-----------------------------------------------------------------------
-  InMage/Involflt: Oct 23 2024 [ 02:41:25 ]
-  Involflt Kernel: 9.63.1.7235
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-automation.txt b/rca-tool/cli/test-output/scc_test-automation.txt
deleted file mode 100644
index 52f3e41..0000000
--- a/rca-tool/cli/test-output/scc_test-automation.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-automation.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-network-tuned.txt b/rca-tool/cli/test-output/scc_test-azure-network-tuned.txt
deleted file mode 100644
index 6e85471..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-network-tuned.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-network-tuned.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-network-warnings.txt b/rca-tool/cli/test-output/scc_test-azure-network-warnings.txt
deleted file mode 100644
index 0080b06..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-network-warnings.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-network-warnings.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-vm-rhel-byos.txt b/rca-tool/cli/test-output/scc_test-azure-vm-rhel-byos.txt
deleted file mode 100644
index 12255bc..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-vm-rhel-byos.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-vm-rhel-byos.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  VM Size: Standard_E16s_v3
-  Publisher: RedHat
-  Offer: rhel-byos
-  SKU: rhel-lvm84
-  Billing Model: BYOS
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-vm-rhel-payg.txt b/rca-tool/cli/test-output/scc_test-azure-vm-rhel-payg.txt
deleted file mode 100644
index 3bde5dc..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-vm-rhel-payg.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-vm-rhel-payg.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  VM Size: Standard_E16s_v3
-  Publisher: RedHat
-  Offer: RHEL-SAP-HA
-  SKU: 8.2
-  Billing Model: PAYG
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-vm-scc-metadata.txt b/rca-tool/cli/test-output/scc_test-azure-vm-scc-metadata.txt
deleted file mode 100644
index c302ba2..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-vm-scc-metadata.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-vm-scc-metadata.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 2
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  VM Size: Standard_M64ds_v2
-  Publisher: SUSE
-  Offer: sles-sap-15-sp5
-  SKU: gen2
-  Billing Model: PAYG
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-vm-sles-payg.txt b/rca-tool/cli/test-output/scc_test-azure-vm-sles-payg.txt
deleted file mode 100644
index 1cabb9e..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-vm-sles-payg.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-vm-sles-payg.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  VM Size: Standard_M32ts
-  Publisher: SUSE
-  Offer: sles-sap-15-sp3
-  SKU: gen2
-  Billing Model: PAYG
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-vm-storage.txt b/rca-tool/cli/test-output/scc_test-azure-vm-storage.txt
deleted file mode 100644
index 0679384..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-vm-storage.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-vm-storage.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  VM Size: Standard_E32s_v3
-  Publisher: SUSE
-  Offer: sles-sap-15-sp5
-  SKU: gen2
-  Billing Model: PAYG
-  OS Disk Type: Premium_LRS
-  Data Disks: 3
-    - LUN 0: UltraSSD_LRS (512 GB)
-    - LUN 1: PremiumV2_LRS (256 GB)
-    - LUN 2: Premium_LRS (1024 GB)
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-vm-ubuntu-byos.txt b/rca-tool/cli/test-output/scc_test-azure-vm-ubuntu-byos.txt
deleted file mode 100644
index 67228fd..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-vm-ubuntu-byos.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-vm-ubuntu-byos.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  VM Size: Standard_D4s_v3
-  Publisher: Canonical
-  Offer: 0001-com-ubuntu-server-focal
-  SKU: 20_04-lts
-  Billing Model: BYOS
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-vm-ubuntu-pro.txt b/rca-tool/cli/test-output/scc_test-azure-vm-ubuntu-pro.txt
deleted file mode 100644
index 15a492e..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-vm-ubuntu-pro.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-vm-ubuntu-pro.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  VM Size: Standard_D4s_v3
-  Publisher: Canonical
-  Offer: 0001-com-ubuntu-pro-focal
-  SKU: pro-20_04-lts
-  Billing Model: PAYG
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-azure-vm.txt b/rca-tool/cli/test-output/scc_test-azure-vm.txt
deleted file mode 100644
index 0457912..0000000
--- a/rca-tool/cli/test-output/scc_test-azure-vm.txt
+++ /dev/null
@@ -1,25 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-azure-vm.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 2
-Directories: 2
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  VM Size: Standard_E4s_v3
-  Publisher: SUSE
-  Offer: sles-sap-15-sp4-byos
-  SKU: gen2
-  Billing Model: BYOS
-  Operating System:
-    Distribution: SUSE Linux Enterprise Server 15 SP4
-    Version: 15.4
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-btrfs.txt b/rca-tool/cli/test-output/scc_test-btrfs.txt
deleted file mode 100644
index 6ea8c12..0000000
--- a/rca-tool/cli/test-output/scc_test-btrfs.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-btrfs.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 2
-
-----------------------------------------------------------------------
-STORAGE
-----------------------------------------------------------------------
-  BTRFS Filesystems: 3
-  BTRFS Subvolumes: 10
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-cluster-nodes.txt b/rca-tool/cli/test-output/scc_test-cluster-nodes.txt
deleted file mode 100644
index c13e9ef..0000000
--- a/rca-tool/cli/test-output/scc_test-cluster-nodes.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-cluster-nodes.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 3
-Directories: 2
-
-----------------------------------------------------------------------
-CLUSTER CONFIGURATION
-----------------------------------------------------------------------
-  Fencing:
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-complete-network-tuning.txt b/rca-tool/cli/test-output/scc_test-complete-network-tuning.txt
deleted file mode 100644
index 94de8c8..0000000
--- a/rca-tool/cli/test-output/scc_test-complete-network-tuning.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-complete-network-tuning.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-corosync-config.txt b/rca-tool/cli/test-output/scc_test-corosync-config.txt
deleted file mode 100644
index dd32991..0000000
--- a/rca-tool/cli/test-output/scc_test-corosync-config.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-corosync-config.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-crm-report-sysinfo.txt b/rca-tool/cli/test-output/scc_test-crm-report-sysinfo.txt
deleted file mode 100644
index 7dc6ee2..0000000
--- a/rca-tool/cli/test-output/scc_test-crm-report-sysinfo.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-crm-report-sysinfo.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  Operating System:
-    Distribution: SUSE Linux Enterprise Server 15 SP5
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-dlm-service.txt b/rca-tool/cli/test-output/scc_test-dlm-service.txt
deleted file mode 100644
index 9bd2ad2..0000000
--- a/rca-tool/cli/test-output/scc_test-dlm-service.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-dlm-service.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 2
-Directories: 4
-
-----------------------------------------------------------------------
-AZURE VM PROPERTIES
-----------------------------------------------------------------------
-  Operating System:
-    Distribution: Red Hat Enterprise Linux 8.6 (Ootpa)
-    Version: 8.6
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-fencing-pcs.txt b/rca-tool/cli/test-output/scc_test-fencing-pcs.txt
deleted file mode 100644
index 505008e..0000000
--- a/rca-tool/cli/test-output/scc_test-fencing-pcs.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-fencing-pcs.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-----------------------------------------------------------------------
-CLUSTER CONFIGURATION
-----------------------------------------------------------------------
-  Pacemaker Resources (1):
-    - hana_scale: SAPHanaController
-  Fencing:
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-fencing.txt b/rca-tool/cli/test-output/scc_test-fencing.txt
deleted file mode 100644
index 5c34906..0000000
--- a/rca-tool/cli/test-output/scc_test-fencing.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-fencing.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-----------------------------------------------------------------------
-CLUSTER CONFIGURATION
-----------------------------------------------------------------------
-  Fencing:
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-fstab.txt b/rca-tool/cli/test-output/scc_test-fstab.txt
deleted file mode 100644
index 7d26cdd..0000000
--- a/rca-tool/cli/test-output/scc_test-fstab.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-fstab.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 2
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-huge-pages-none.txt b/rca-tool/cli/test-output/scc_test-huge-pages-none.txt
deleted file mode 100644
index 859e512..0000000
--- a/rca-tool/cli/test-output/scc_test-huge-pages-none.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-huge-pages-none.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 3
-Directories: 6
-
-----------------------------------------------------------------------
-KERNEL AND SYSTEM
-----------------------------------------------------------------------
-  Huge Pages:
-    Static: 0 MB
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-huge-pages.txt b/rca-tool/cli/test-output/scc_test-huge-pages.txt
deleted file mode 100644
index 17d0cb9..0000000
--- a/rca-tool/cli/test-output/scc_test-huge-pages.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-huge-pages.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 3
-Directories: 6
-
-----------------------------------------------------------------------
-KERNEL AND SYSTEM
-----------------------------------------------------------------------
-  Huge Pages:
-    Static: 4096 MB
-    Transparent: 2048 MB
-    [INFO] Transparent Huge Pages (THP) are being used (2048 MB). For SAP HANA, THP should be disabled as it can cause performance issues.
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-illumio.txt b/rca-tool/cli/test-output/scc_test-illumio.txt
deleted file mode 100644
index 3e3a95f..0000000
--- a/rca-tool/cli/test-output/scc_test-illumio.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-illumio.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 7
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-kernel-reboots.txt b/rca-tool/cli/test-output/scc_test-kernel-reboots.txt
deleted file mode 100644
index 5820036..0000000
--- a/rca-tool/cli/test-output/scc_test-kernel-reboots.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-kernel-reboots.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-kernel-tuning-warnings.txt b/rca-tool/cli/test-output/scc_test-kernel-tuning-warnings.txt
deleted file mode 100644
index dec7aac..0000000
--- a/rca-tool/cli/test-output/scc_test-kernel-tuning-warnings.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-kernel-tuning-warnings.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-kernel-tuning.txt b/rca-tool/cli/test-output/scc_test-kernel-tuning.txt
deleted file mode 100644
index 4f9945e..0000000
--- a/rca-tool/cli/test-output/scc_test-kernel-tuning.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-kernel-tuning.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-live-migration.txt b/rca-tool/cli/test-output/scc_test-live-migration.txt
deleted file mode 100644
index d8f72ba..0000000
--- a/rca-tool/cli/test-output/scc_test-live-migration.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-live-migration.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-lvm.txt b/rca-tool/cli/test-output/scc_test-lvm.txt
deleted file mode 100644
index b39e03d..0000000
--- a/rca-tool/cli/test-output/scc_test-lvm.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-lvm.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 6
-Directories: 2
-
-----------------------------------------------------------------------
-STORAGE
-----------------------------------------------------------------------
-  LVM:
-    Physical Volumes: 2
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-nested-gzip.txt b/rca-tool/cli/test-output/scc_test-nested-gzip.txt
deleted file mode 100644
index d9b3b43..0000000
--- a/rca-tool/cli/test-output/scc_test-nested-gzip.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-nested-gzip.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 6
-Directories: 5
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-oom-killer.txt b/rca-tool/cli/test-output/scc_test-oom-killer.txt
deleted file mode 100644
index db89915..0000000
--- a/rca-tool/cli/test-output/scc_test-oom-killer.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-oom-killer.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-optional-network-tuning.txt b/rca-tool/cli/test-output/scc_test-optional-network-tuning.txt
deleted file mode 100644
index 2541b04..0000000
--- a/rca-tool/cli/test-output/scc_test-optional-network-tuning.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-optional-network-tuning.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-pacemaker-resources.txt b/rca-tool/cli/test-output/scc_test-pacemaker-resources.txt
deleted file mode 100644
index 57155d0..0000000
--- a/rca-tool/cli/test-output/scc_test-pacemaker-resources.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-pacemaker-resources.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-----------------------------------------------------------------------
-CLUSTER CONFIGURATION
-----------------------------------------------------------------------
-  Pacemaker Resources (3):
-    - rsc_SAPHana_HDB_HDB00: SAPHana
-    - rsc_ip_HDB_HDB00: IPaddr2
-    - stonith-sbd: sbd
-  Fencing:
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-raid.txt b/rca-tool/cli/test-output/scc_test-raid.txt
deleted file mode 100644
index 212587d..0000000
--- a/rca-tool/cli/test-output/scc_test-raid.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-raid.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 4
-Directories: 3
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-systemd-messages.txt b/rca-tool/cli/test-output/scc_test-systemd-messages.txt
deleted file mode 100644
index 18cc7c4..0000000
--- a/rca-tool/cli/test-output/scc_test-systemd-messages.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-systemd-messages.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-trendmicro.txt b/rca-tool/cli/test-output/scc_test-trendmicro.txt
deleted file mode 100644
index e9249bc..0000000
--- a/rca-tool/cli/test-output/scc_test-trendmicro.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-trendmicro.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 7
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-xfs-duplicate-uuid.txt b/rca-tool/cli/test-output/scc_test-xfs-duplicate-uuid.txt
deleted file mode 100644
index 3cf29a1..0000000
--- a/rca-tool/cli/test-output/scc_test-xfs-duplicate-uuid.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-xfs-duplicate-uuid.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 1
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-xfs-errors.txt b/rca-tool/cli/test-output/scc_test-xfs-errors.txt
deleted file mode 100644
index dda36df..0000000
--- a/rca-tool/cli/test-output/scc_test-xfs-errors.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-xfs-errors.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 3
-Directories: 1
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/scc_test-xfs-timestamp-normalization.txt b/rca-tool/cli/test-output/scc_test-xfs-timestamp-normalization.txt
deleted file mode 100644
index 62cf562..0000000
--- a/rca-tool/cli/test-output/scc_test-xfs-timestamp-normalization.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/scc_test-xfs-timestamp-normalization.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 2
-Directories: 1
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/sosreport-deb-raw.txt b/rca-tool/cli/test-output/sosreport-deb-raw.txt
deleted file mode 100644
index 09e4d07..0000000
--- a/rca-tool/cli/test-output/sosreport-deb-raw.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/sosreport-deb-raw.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-----------------------------------------------------------------------
-DISTRIBUTION
-----------------------------------------------------------------------
-  Packages: 22 installed
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/sosreport-rpm-raw.txt b/rca-tool/cli/test-output/sosreport-rpm-raw.txt
deleted file mode 100644
index 36fdb1a..0000000
--- a/rca-tool/cli/test-output/sosreport-rpm-raw.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/sosreport-rpm-raw.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-----------------------------------------------------------------------
-DISTRIBUTION
-----------------------------------------------------------------------
-  Packages: 16 installed
-
-======================================================================
-
diff --git a/rca-tool/cli/test-output/sosreport-yum-raw.txt b/rca-tool/cli/test-output/sosreport-yum-raw.txt
deleted file mode 100644
index bcab1d7..0000000
--- a/rca-tool/cli/test-output/sosreport-yum-raw.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-
-Analyzing: /home/fede2/proyectos/azure-support-scripts/rca-tool/cli/../tests/fixtures/sosreport-yum-raw.tar.xz
-
-
-======================================================================
-  RCA Analysis Results
-======================================================================
-
-Files processed: 1
-Directories: 3
-
-----------------------------------------------------------------------
-DISTRIBUTION
-----------------------------------------------------------------------
-  Packages: 17 installed
-
-======================================================================
-