diff --git a/ai-analyzer/cli.js b/ai-analyzer/cli.js index 85a80a2..de4d742 100644 --- a/ai-analyzer/cli.js +++ b/ai-analyzer/cli.js @@ -1,250 +1,250 @@ -#!/usr/bin/env node - -/** - * AI Analyzer CLI - * - * Command-line interface for the ai-analyzer package - * Can be used by any parser to analyze JSON files - */ - -const fs = require("fs"); -const path = require("path"); - -// Import AI utilities from this package -const { - logger, - analyzeBatch, - checkOllamaStatus, - findLatestResultsFile, -} = require("./index"); - -// Default configuration -const DEFAULT_CONTEXT = - process.env.AI_CONTEXT || "job market analysis and trends"; -const DEFAULT_MODEL = process.env.OLLAMA_MODEL || "mistral"; -const DEFAULT_RESULTS_DIR = "results"; - -// Parse command line arguments -const args = process.argv.slice(2); -let inputFile = null; -let outputFile = null; -let context = DEFAULT_CONTEXT; -let model = DEFAULT_MODEL; -let findLatest = false; -let resultsDir = DEFAULT_RESULTS_DIR; - -for (const arg of args) { - if (arg.startsWith("--input=")) { - inputFile = arg.split("=")[1]; - } else if (arg.startsWith("--output=")) { - outputFile = arg.split("=")[1]; - } else if (arg.startsWith("--context=")) { - context = arg.split("=")[1]; - } else if (arg.startsWith("--model=")) { - model = arg.split("=")[1]; - } else if (arg.startsWith("--dir=")) { - resultsDir = arg.split("=")[1]; - } else if (arg === "--latest") { - findLatest = true; - } else if (arg === "--help" || arg === "-h") { - console.log(` -AI Analyzer CLI - -Usage: node cli.js [options] - -Options: - --input=FILE Input JSON file - --output=FILE Output file (default: ai-analysis-{timestamp}.json) - --context="description" Analysis context (default: "${DEFAULT_CONTEXT}") - --model=MODEL Ollama model (default: ${DEFAULT_MODEL}) - --latest Use latest results file from results directory - --dir=PATH Directory to look for results (default: 'results') - --help, -h Show this help - -Examples: - node cli.js --input=results.json - node cli.js --latest --dir=results - node cli.js --input=results.json --context="job trends" --model=mistral - -Environment Variables: - AI_CONTEXT Default analysis context - OLLAMA_MODEL Default Ollama model -`); - process.exit(0); - } -} - -async function main() { - try { - // Determine input file - if (findLatest) { - try { - inputFile = findLatestResultsFile(resultsDir); - logger.info(`Found latest results file: ${inputFile}`); - } catch (error) { - logger.error( - `āŒ No results files found in '${resultsDir}': ${error.message}` - ); - logger.info(`šŸ’” To create results files:`); - logger.info( - ` 1. Run a parser first (e.g., npm start in linkedin-parser)` - ); - logger.info(` 2. Or provide a specific file with --input=FILE`); - logger.info(` 3. Or create a sample JSON file to test with`); - process.exit(1); - } - } - - // If inputFile is a relative path and --dir is set, resolve it - if (inputFile && !path.isAbsolute(inputFile) && !fs.existsSync(inputFile)) { - const candidate = path.join(resultsDir, inputFile); - if (fs.existsSync(candidate)) { - inputFile = candidate; - } - } - - if (!inputFile) { - logger.error("āŒ Input file required. Use --input=FILE or --latest"); - logger.info(`šŸ’” Examples:`); - logger.info(` node cli.js --input=results.json`); - logger.info(` node cli.js --latest --dir=results`); - logger.info(` node cli.js --help`); - process.exit(1); - } - - // Load input file - logger.step(`Loading input file: ${inputFile}`); - - if (!fs.existsSync(inputFile)) { - throw new Error(`Input file not found: ${inputFile}`); - } - - const data = JSON.parse(fs.readFileSync(inputFile, "utf-8")); - - // Extract posts from different formats - let posts = []; - if (data.results && Array.isArray(data.results)) { - posts = data.results; - logger.info(`Found ${posts.length} items in results array`); - } else if (Array.isArray(data)) { - posts = data; - logger.info(`Found ${posts.length} items in array`); - } else { - throw new Error("Invalid JSON format - need array or {results: [...]}"); - } - - if (posts.length === 0) { - throw new Error("No items found to analyze"); - } - - // Check AI availability - logger.step("Checking AI availability"); - const aiAvailable = await checkOllamaStatus(model); - if (!aiAvailable) { - throw new Error( - `AI not available. Make sure Ollama is running and model '${model}' is installed.` - ); - } - - // Check if results already have AI analysis - const hasExistingAI = posts.some((post) => post.aiAnalysis); - if (hasExistingAI) { - logger.info( - `šŸ“‹ Results already contain AI analysis - will update with new context` - ); - } - - // Prepare data for analysis - const analysisData = posts.map((post, i) => ({ - text: post.text || post.content || post.post || "", - location: post.location || "Unknown", - keyword: post.keyword || "Unknown", - timestamp: post.timestamp || new Date().toISOString(), - })); - - // Run analysis - logger.step(`Running AI analysis with context: "${context}"`); - const analysis = await analyzeBatch(analysisData, context, model); - - // Integrate AI analysis back into the original results - const updatedPosts = posts.map((post, index) => { - const aiResult = analysis[index]; - return { - ...post, - aiAnalysis: { - isRelevant: aiResult.isRelevant, - confidence: aiResult.confidence, - reasoning: aiResult.reasoning, - context: context, - model: model, - analyzedAt: new Date().toISOString(), - }, - }; - }); - - // Update the original data structure - if (data.results && Array.isArray(data.results)) { - data.results = updatedPosts; - // Update metadata - data.metadata = data.metadata || {}; - data.metadata.aiAnalysisUpdated = new Date().toISOString(); - data.metadata.aiContext = context; - data.metadata.aiModel = model; - } else { - // If it's a simple array, create a proper structure - data = { - metadata: { - timestamp: new Date().toISOString(), - totalItems: updatedPosts.length, - aiContext: context, - aiModel: model, - analysisType: "cli", - }, - results: updatedPosts, - }; - } - - // Generate output filename if not provided - if (!outputFile) { - // Use the original filename with -ai suffix - const originalName = path.basename(inputFile, path.extname(inputFile)); - outputFile = path.join( - path.dirname(inputFile), - `${originalName}-ai.json` - ); - } - - // Save updated results back to file - fs.writeFileSync(outputFile, JSON.stringify(data, null, 2)); - - // Show summary - const relevant = analysis.filter((a) => a.isRelevant).length; - const irrelevant = analysis.filter((a) => !a.isRelevant).length; - const avgConfidence = - analysis.reduce((sum, a) => sum + a.confidence, 0) / analysis.length; - - logger.success("āœ… AI analysis completed and integrated"); - logger.info(`šŸ“Š Context: "${context}"`); - logger.info(`šŸ“ˆ Total items analyzed: ${analysis.length}`); - logger.info( - `āœ… Relevant items: ${relevant} (${( - (relevant / analysis.length) * - 100 - ).toFixed(1)}%)` - ); - logger.info( - `āŒ Irrelevant items: ${irrelevant} (${( - (irrelevant / analysis.length) * - 100 - ).toFixed(1)}%)` - ); - logger.info(`šŸŽÆ Average confidence: ${avgConfidence.toFixed(2)}`); - logger.file(`🧠 Updated results saved to: ${outputFile}`); - } catch (error) { - logger.error(`āŒ Analysis failed: ${error.message}`); - process.exit(1); - } -} - -// Run the CLI -main(); +#!/usr/bin/env node + +/** + * AI Analyzer CLI + * + * Command-line interface for the ai-analyzer package + * Can be used by any parser to analyze JSON files + */ + +const fs = require("fs"); +const path = require("path"); + +// Import AI utilities from this package +const { + logger, + analyzeBatch, + checkOllamaStatus, + findLatestResultsFile, +} = require("./index"); + +// Default configuration +const DEFAULT_CONTEXT = + process.env.AI_CONTEXT || "job market analysis and trends"; +const DEFAULT_MODEL = process.env.OLLAMA_MODEL || "mistral"; +const DEFAULT_RESULTS_DIR = "results"; + +// Parse command line arguments +const args = process.argv.slice(2); +let inputFile = null; +let outputFile = null; +let context = DEFAULT_CONTEXT; +let model = DEFAULT_MODEL; +let findLatest = false; +let resultsDir = DEFAULT_RESULTS_DIR; + +for (const arg of args) { + if (arg.startsWith("--input=")) { + inputFile = arg.split("=")[1]; + } else if (arg.startsWith("--output=")) { + outputFile = arg.split("=")[1]; + } else if (arg.startsWith("--context=")) { + context = arg.split("=")[1]; + } else if (arg.startsWith("--model=")) { + model = arg.split("=")[1]; + } else if (arg.startsWith("--dir=")) { + resultsDir = arg.split("=")[1]; + } else if (arg === "--latest") { + findLatest = true; + } else if (arg === "--help" || arg === "-h") { + console.log(` +AI Analyzer CLI + +Usage: node cli.js [options] + +Options: + --input=FILE Input JSON file + --output=FILE Output file (default: ai-analysis-{timestamp}.json) + --context="description" Analysis context (default: "${DEFAULT_CONTEXT}") + --model=MODEL Ollama model (default: ${DEFAULT_MODEL}) + --latest Use latest results file from results directory + --dir=PATH Directory to look for results (default: 'results') + --help, -h Show this help + +Examples: + node cli.js --input=results.json + node cli.js --latest --dir=results + node cli.js --input=results.json --context="job trends" --model=mistral + +Environment Variables: + AI_CONTEXT Default analysis context + OLLAMA_MODEL Default Ollama model +`); + process.exit(0); + } +} + +async function main() { + try { + // Determine input file + if (findLatest) { + try { + inputFile = findLatestResultsFile(resultsDir); + logger.info(`Found latest results file: ${inputFile}`); + } catch (error) { + logger.error( + `āŒ No results files found in '${resultsDir}': ${error.message}` + ); + logger.info(`šŸ’” To create results files:`); + logger.info( + ` 1. Run a parser first (e.g., npm start in linkedin-parser)` + ); + logger.info(` 2. Or provide a specific file with --input=FILE`); + logger.info(` 3. Or create a sample JSON file to test with`); + process.exit(1); + } + } + + // If inputFile is a relative path and --dir is set, resolve it + if (inputFile && !path.isAbsolute(inputFile) && !fs.existsSync(inputFile)) { + const candidate = path.join(resultsDir, inputFile); + if (fs.existsSync(candidate)) { + inputFile = candidate; + } + } + + if (!inputFile) { + logger.error("āŒ Input file required. Use --input=FILE or --latest"); + logger.info(`šŸ’” Examples:`); + logger.info(` node cli.js --input=results.json`); + logger.info(` node cli.js --latest --dir=results`); + logger.info(` node cli.js --help`); + process.exit(1); + } + + // Load input file + logger.step(`Loading input file: ${inputFile}`); + + if (!fs.existsSync(inputFile)) { + throw new Error(`Input file not found: ${inputFile}`); + } + + const data = JSON.parse(fs.readFileSync(inputFile, "utf-8")); + + // Extract posts from different formats + let posts = []; + if (data.results && Array.isArray(data.results)) { + posts = data.results; + logger.info(`Found ${posts.length} items in results array`); + } else if (Array.isArray(data)) { + posts = data; + logger.info(`Found ${posts.length} items in array`); + } else { + throw new Error("Invalid JSON format - need array or {results: [...]}"); + } + + if (posts.length === 0) { + throw new Error("No items found to analyze"); + } + + // Check AI availability + logger.step("Checking AI availability"); + const aiAvailable = await checkOllamaStatus(model); + if (!aiAvailable) { + throw new Error( + `AI not available. Make sure Ollama is running and model '${model}' is installed.` + ); + } + + // Check if results already have AI analysis + const hasExistingAI = posts.some((post) => post.aiAnalysis); + if (hasExistingAI) { + logger.info( + `šŸ“‹ Results already contain AI analysis - will update with new context` + ); + } + + // Prepare data for analysis + const analysisData = posts.map((post, i) => ({ + text: post.text || post.content || post.post || "", + location: post.location || "Unknown", + keyword: post.keyword || "Unknown", + timestamp: post.timestamp || new Date().toISOString(), + })); + + // Run analysis + logger.step(`Running AI analysis with context: "${context}"`); + const analysis = await analyzeBatch(analysisData, context, model); + + // Integrate AI analysis back into the original results + const updatedPosts = posts.map((post, index) => { + const aiResult = analysis[index]; + return { + ...post, + aiAnalysis: { + isRelevant: aiResult.isRelevant, + confidence: aiResult.confidence, + reasoning: aiResult.reasoning, + context: context, + model: model, + analyzedAt: new Date().toISOString(), + }, + }; + }); + + // Update the original data structure + if (data.results && Array.isArray(data.results)) { + data.results = updatedPosts; + // Update metadata + data.metadata = data.metadata || {}; + data.metadata.aiAnalysisUpdated = new Date().toISOString(); + data.metadata.aiContext = context; + data.metadata.aiModel = model; + } else { + // If it's a simple array, create a proper structure + data = { + metadata: { + timestamp: new Date().toISOString(), + totalItems: updatedPosts.length, + aiContext: context, + aiModel: model, + analysisType: "cli", + }, + results: updatedPosts, + }; + } + + // Generate output filename if not provided + if (!outputFile) { + // Use the original filename with -ai suffix + const originalName = path.basename(inputFile, path.extname(inputFile)); + outputFile = path.join( + path.dirname(inputFile), + `${originalName}-ai.json` + ); + } + + // Save updated results back to file + fs.writeFileSync(outputFile, JSON.stringify(data, null, 2)); + + // Show summary + const relevant = analysis.filter((a) => a.isRelevant).length; + const irrelevant = analysis.filter((a) => !a.isRelevant).length; + const avgConfidence = + analysis.reduce((sum, a) => sum + a.confidence, 0) / analysis.length; + + logger.success("āœ… AI analysis completed and integrated"); + logger.info(`šŸ“Š Context: "${context}"`); + logger.info(`šŸ“ˆ Total items analyzed: ${analysis.length}`); + logger.info( + `āœ… Relevant items: ${relevant} (${( + (relevant / analysis.length) * + 100 + ).toFixed(1)}%)` + ); + logger.info( + `āŒ Irrelevant items: ${irrelevant} (${( + (irrelevant / analysis.length) * + 100 + ).toFixed(1)}%)` + ); + logger.info(`šŸŽÆ Average confidence: ${avgConfidence.toFixed(2)}`); + logger.file(`🧠 Updated results saved to: ${outputFile}`); + } catch (error) { + logger.error(`āŒ Analysis failed: ${error.message}`); + process.exit(1); + } +} + +// Run the CLI +main(); diff --git a/ai-analyzer/demo.js b/ai-analyzer/demo.js index 6e70983..24e775f 100644 --- a/ai-analyzer/demo.js +++ b/ai-analyzer/demo.js @@ -1,346 +1,346 @@ -/** - * AI Analyzer Demo - * - * Demonstrates all the core utilities provided by the ai-analyzer package: - * - Logger functionality - * - Text processing utilities - * - Location validation - * - AI analysis capabilities - * - Test utilities - */ - -const { - logger, - Logger, - cleanText, - containsAnyKeyword, - parseLocationFilters, - validateLocationAgainstFilters, - extractLocationFromProfile, - analyzeBatch, -} = require("./index"); - -// Terminal colors for demo output -const colors = { - reset: "\x1b[0m", - bright: "\x1b[1m", - cyan: "\x1b[36m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - magenta: "\x1b[35m", - red: "\x1b[31m", -}; - -const demo = { - title: (text) => - console.log(`\n${colors.bright}${colors.cyan}${text}${colors.reset}`), - section: (text) => - console.log(`\n${colors.bright}${colors.magenta}${text}${colors.reset}`), - success: (text) => console.log(`${colors.green}āœ… ${text}${colors.reset}`), - info: (text) => console.log(`${colors.blue}ā„¹ļø ${text}${colors.reset}`), - warning: (text) => console.log(`${colors.yellow}āš ļø ${text}${colors.reset}`), - error: (text) => console.log(`${colors.red}āŒ ${text}${colors.reset}`), - code: (text) => console.log(`${colors.cyan}${text}${colors.reset}`), -}; - -async function runDemo() { - demo.title("=== AI Analyzer Demo ==="); - demo.info( - "This demo showcases all the core utilities provided by the ai-analyzer package." - ); - demo.info("Press Enter to continue through each section...\n"); - - await waitForEnter(); - - // 1. Logger Demo - await demonstrateLogger(); - - // 2. Text Processing Demo - await demonstrateTextProcessing(); - - // 3. Location Validation Demo - await demonstrateLocationValidation(); - - // 4. AI Analysis Demo - await demonstrateAIAnalysis(); - - // 5. Integration Demo - await demonstrateIntegration(); - - demo.title("=== Demo Complete ==="); - demo.success("All ai-analyzer utilities demonstrated successfully!"); - demo.info("Check the README.md for detailed API documentation."); -} - -async function demonstrateLogger() { - demo.section("1. Logger Utilities"); - demo.info( - "The logger provides consistent logging across all parsers with configurable levels and color support." - ); - - demo.code("// Using default logger"); - logger.info("This is an info message"); - logger.warning("This is a warning message"); - logger.error("This is an error message"); - logger.success("This is a success message"); - logger.debug("This is a debug message (if enabled)"); - - demo.code("// Convenience methods with emoji prefixes"); - logger.step("Starting demo process"); - logger.search("Searching for keywords"); - logger.ai("Running AI analysis"); - logger.location("Validating location"); - logger.file("Saving results"); - - demo.code("// Custom logger configuration"); - const customLogger = new Logger({ - debug: false, - colors: true, - }); - customLogger.info("Custom logger with debug disabled"); - customLogger.debug("This won't show"); - - demo.code("// Silent mode"); - const silentLogger = new Logger(); - silentLogger.silent(); - silentLogger.info("This won't show"); - silentLogger.verbose(); // Re-enable all levels - - await waitForEnter(); -} - -async function demonstrateTextProcessing() { - demo.section("2. Text Processing Utilities"); - demo.info( - "Text utilities provide content cleaning and keyword matching capabilities." - ); - - const sampleTexts = [ - "Check out this #awesome post! https://example.com šŸš€", - "Just got #laidoff from my job. Looking for new opportunities!", - "Company is #downsizing and I'm affected. #RIF #layoff", - "Great news! We're #hiring new developers! šŸŽ‰", - ]; - - demo.code("// Text cleaning examples:"); - sampleTexts.forEach((text, index) => { - const cleaned = cleanText(text); - demo.info(`Original: ${text}`); - demo.success(`Cleaned: ${cleaned}`); - console.log(); - }); - - demo.code("// Keyword matching:"); - const keywords = ["layoff", "downsizing", "RIF", "hiring"]; - - sampleTexts.forEach((text, index) => { - const hasMatch = containsAnyKeyword(text, keywords); - const matchedKeywords = keywords.filter((keyword) => - text.toLowerCase().includes(keyword.toLowerCase()) - ); - - demo.info( - `Text ${index + 1}: ${hasMatch ? "āœ…" : "āŒ"} ${ - matchedKeywords.join(", ") || "No matches" - }` - ); - }); - - await waitForEnter(); -} - -async function demonstrateLocationValidation() { - demo.section("3. Location Validation Utilities"); - demo.info( - "Location utilities provide geographic filtering and validation capabilities." - ); - - demo.code("// Location filter parsing:"); - const filterStrings = [ - "Ontario,Manitoba", - "Toronto,Vancouver", - "British Columbia,Alberta", - "Canada", - ]; - - filterStrings.forEach((filterString) => { - const filters = parseLocationFilters(filterString); - demo.info(`Filter: "${filterString}"`); - demo.success(`Parsed: [${filters.join(", ")}]`); - console.log(); - }); - - demo.code("// Location validation examples:"); - const testLocations = [ - { location: "Toronto, Ontario, Canada", filters: ["Ontario"] }, - { location: "Vancouver, BC", filters: ["British Columbia"] }, - { location: "Calgary, Alberta", filters: ["Ontario"] }, - { location: "Montreal, Quebec", filters: ["Ontario", "Manitoba"] }, - { location: "New York, NY", filters: ["Ontario"] }, - ]; - - testLocations.forEach(({ location, filters }) => { - const isValid = validateLocationAgainstFilters(location, filters); - demo.info(`Location: "${location}"`); - demo.info(`Filters: [${filters.join(", ")}]`); - demo.success(`Valid: ${isValid ? "āœ… Yes" : "āŒ No"}`); - console.log(); - }); - - demo.code("// Profile location extraction:"); - const profileTexts = [ - "Software Engineer at Tech Corp • Toronto, Ontario", - "Product Manager • Vancouver, BC", - "Data Scientist • Remote", - "CEO at Startup Inc • Montreal, Quebec, Canada", - ]; - - profileTexts.forEach((profileText) => { - const location = extractLocationFromProfile(profileText); - demo.info(`Profile: "${profileText}"`); - demo.success(`Extracted: "${location || "No location found"}"`); - console.log(); - }); - - await waitForEnter(); -} - -async function demonstrateAIAnalysis() { - demo.section("4. AI Analysis Utilities"); - demo.info( - "AI utilities provide content analysis using OpenAI or local Ollama models." - ); - - // Mock posts for demo - const mockPosts = [ - { - id: "1", - content: - "Just got laid off from my software engineering role. Looking for new opportunities in Toronto.", - author: "John Doe", - location: "Toronto, Ontario", - }, - { - id: "2", - content: - "Our company is downsizing and I'm affected. This is really tough news.", - author: "Jane Smith", - location: "Vancouver, BC", - }, - { - id: "3", - content: - "We're hiring! Looking for talented developers to join our team.", - author: "Bob Wilson", - location: "Calgary, Alberta", - }, - ]; - - demo.code("// Mock AI analysis (simulated):"); - demo.info("In a real scenario, this would call Ollama or OpenAI API"); - - mockPosts.forEach((post, index) => { - demo.info(`Post ${index + 1}: ${post.content.substring(0, 50)}...`); - demo.success( - `Analysis: Relevant to job layoffs (confidence: 0.${85 + index * 5})` - ); - console.log(); - }); - - demo.code("// Batch analysis simulation:"); - demo.info("Processing batch of 3 posts..."); - await simulateProcessing(); - demo.success("Batch analysis completed!"); - - await waitForEnter(); -} - -async function demonstrateIntegration() { - demo.section("5. Integration Example"); - demo.info("Here's how all utilities work together in a real scenario:"); - - const samplePost = { - id: "demo-1", - content: - "Just got #laidoff from my job at TechCorp! Looking for new opportunities in #Toronto. This is really tough but I'm staying positive! šŸš€", - author: "Demo User", - location: "Toronto, Ontario, Canada", - }; - - demo.code("// Processing pipeline:"); - - // 1. Log the start - logger.step("Processing new post"); - - // 2. Clean the text - const cleanedContent = cleanText(samplePost.content); - logger.info(`Cleaned content: ${cleanedContent}`); - - // 3. Check for keywords - const keywords = ["layoff", "downsizing", "RIF"]; - const hasKeywords = containsAnyKeyword(cleanedContent, keywords); - logger.search(`Keyword match: ${hasKeywords ? "Found" : "Not found"}`); - - // 4. Validate location - const locationFilters = parseLocationFilters("Ontario,Manitoba"); - const isValidLocation = validateLocationAgainstFilters( - samplePost.location, - locationFilters - ); - logger.location(`Location valid: ${isValidLocation ? "Yes" : "No"}`); - - // 5. Simulate AI analysis - if (hasKeywords && isValidLocation) { - logger.ai("Running AI analysis..."); - await simulateProcessing(); - logger.success("Post accepted and analyzed!"); - } else { - logger.warning("Post rejected - doesn't meet criteria"); - } - - await waitForEnter(); -} - -// Helper functions -function waitForEnter() { - return new Promise((resolve) => { - const readline = require("readline"); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rl.question("\nPress Enter to continue...", () => { - rl.close(); - resolve(); - }); - }); -} - -async function simulateProcessing() { - return new Promise((resolve) => { - const dots = [".", "..", "..."]; - let i = 0; - const interval = setInterval(() => { - process.stdout.write(`\rProcessing${dots[i]}`); - i = (i + 1) % dots.length; - }, 500); - - setTimeout(() => { - clearInterval(interval); - process.stdout.write("\r"); - resolve(); - }, 2000); - }); -} - -// Run the demo if this file is executed directly -if (require.main === module) { - runDemo().catch((error) => { - demo.error(`Demo failed: ${error.message}`); - process.exit(1); - }); -} - -module.exports = { runDemo }; +/** + * AI Analyzer Demo + * + * Demonstrates all the core utilities provided by the ai-analyzer package: + * - Logger functionality + * - Text processing utilities + * - Location validation + * - AI analysis capabilities + * - Test utilities + */ + +const { + logger, + Logger, + cleanText, + containsAnyKeyword, + parseLocationFilters, + validateLocationAgainstFilters, + extractLocationFromProfile, + analyzeBatch, +} = require("./index"); + +// Terminal colors for demo output +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + cyan: "\x1b[36m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + red: "\x1b[31m", +}; + +const demo = { + title: (text) => + console.log(`\n${colors.bright}${colors.cyan}${text}${colors.reset}`), + section: (text) => + console.log(`\n${colors.bright}${colors.magenta}${text}${colors.reset}`), + success: (text) => console.log(`${colors.green}āœ… ${text}${colors.reset}`), + info: (text) => console.log(`${colors.blue}ā„¹ļø ${text}${colors.reset}`), + warning: (text) => console.log(`${colors.yellow}āš ļø ${text}${colors.reset}`), + error: (text) => console.log(`${colors.red}āŒ ${text}${colors.reset}`), + code: (text) => console.log(`${colors.cyan}${text}${colors.reset}`), +}; + +async function runDemo() { + demo.title("=== AI Analyzer Demo ==="); + demo.info( + "This demo showcases all the core utilities provided by the ai-analyzer package." + ); + demo.info("Press Enter to continue through each section...\n"); + + await waitForEnter(); + + // 1. Logger Demo + await demonstrateLogger(); + + // 2. Text Processing Demo + await demonstrateTextProcessing(); + + // 3. Location Validation Demo + await demonstrateLocationValidation(); + + // 4. AI Analysis Demo + await demonstrateAIAnalysis(); + + // 5. Integration Demo + await demonstrateIntegration(); + + demo.title("=== Demo Complete ==="); + demo.success("All ai-analyzer utilities demonstrated successfully!"); + demo.info("Check the README.md for detailed API documentation."); +} + +async function demonstrateLogger() { + demo.section("1. Logger Utilities"); + demo.info( + "The logger provides consistent logging across all parsers with configurable levels and color support." + ); + + demo.code("// Using default logger"); + logger.info("This is an info message"); + logger.warning("This is a warning message"); + logger.error("This is an error message"); + logger.success("This is a success message"); + logger.debug("This is a debug message (if enabled)"); + + demo.code("// Convenience methods with emoji prefixes"); + logger.step("Starting demo process"); + logger.search("Searching for keywords"); + logger.ai("Running AI analysis"); + logger.location("Validating location"); + logger.file("Saving results"); + + demo.code("// Custom logger configuration"); + const customLogger = new Logger({ + debug: false, + colors: true, + }); + customLogger.info("Custom logger with debug disabled"); + customLogger.debug("This won't show"); + + demo.code("// Silent mode"); + const silentLogger = new Logger(); + silentLogger.silent(); + silentLogger.info("This won't show"); + silentLogger.verbose(); // Re-enable all levels + + await waitForEnter(); +} + +async function demonstrateTextProcessing() { + demo.section("2. Text Processing Utilities"); + demo.info( + "Text utilities provide content cleaning and keyword matching capabilities." + ); + + const sampleTexts = [ + "Check out this #awesome post! https://example.com šŸš€", + "Just got #laidoff from my job. Looking for new opportunities!", + "Company is #downsizing and I'm affected. #RIF #layoff", + "Great news! We're #hiring new developers! šŸŽ‰", + ]; + + demo.code("// Text cleaning examples:"); + sampleTexts.forEach((text, index) => { + const cleaned = cleanText(text); + demo.info(`Original: ${text}`); + demo.success(`Cleaned: ${cleaned}`); + console.log(); + }); + + demo.code("// Keyword matching:"); + const keywords = ["layoff", "downsizing", "RIF", "hiring"]; + + sampleTexts.forEach((text, index) => { + const hasMatch = containsAnyKeyword(text, keywords); + const matchedKeywords = keywords.filter((keyword) => + text.toLowerCase().includes(keyword.toLowerCase()) + ); + + demo.info( + `Text ${index + 1}: ${hasMatch ? "āœ…" : "āŒ"} ${ + matchedKeywords.join(", ") || "No matches" + }` + ); + }); + + await waitForEnter(); +} + +async function demonstrateLocationValidation() { + demo.section("3. Location Validation Utilities"); + demo.info( + "Location utilities provide geographic filtering and validation capabilities." + ); + + demo.code("// Location filter parsing:"); + const filterStrings = [ + "Ontario,Manitoba", + "Toronto,Vancouver", + "British Columbia,Alberta", + "Canada", + ]; + + filterStrings.forEach((filterString) => { + const filters = parseLocationFilters(filterString); + demo.info(`Filter: "${filterString}"`); + demo.success(`Parsed: [${filters.join(", ")}]`); + console.log(); + }); + + demo.code("// Location validation examples:"); + const testLocations = [ + { location: "Toronto, Ontario, Canada", filters: ["Ontario"] }, + { location: "Vancouver, BC", filters: ["British Columbia"] }, + { location: "Calgary, Alberta", filters: ["Ontario"] }, + { location: "Montreal, Quebec", filters: ["Ontario", "Manitoba"] }, + { location: "New York, NY", filters: ["Ontario"] }, + ]; + + testLocations.forEach(({ location, filters }) => { + const isValid = validateLocationAgainstFilters(location, filters); + demo.info(`Location: "${location}"`); + demo.info(`Filters: [${filters.join(", ")}]`); + demo.success(`Valid: ${isValid ? "āœ… Yes" : "āŒ No"}`); + console.log(); + }); + + demo.code("// Profile location extraction:"); + const profileTexts = [ + "Software Engineer at Tech Corp • Toronto, Ontario", + "Product Manager • Vancouver, BC", + "Data Scientist • Remote", + "CEO at Startup Inc • Montreal, Quebec, Canada", + ]; + + profileTexts.forEach((profileText) => { + const location = extractLocationFromProfile(profileText); + demo.info(`Profile: "${profileText}"`); + demo.success(`Extracted: "${location || "No location found"}"`); + console.log(); + }); + + await waitForEnter(); +} + +async function demonstrateAIAnalysis() { + demo.section("4. AI Analysis Utilities"); + demo.info( + "AI utilities provide content analysis using OpenAI or local Ollama models." + ); + + // Mock posts for demo + const mockPosts = [ + { + id: "1", + content: + "Just got laid off from my software engineering role. Looking for new opportunities in Toronto.", + author: "John Doe", + location: "Toronto, Ontario", + }, + { + id: "2", + content: + "Our company is downsizing and I'm affected. This is really tough news.", + author: "Jane Smith", + location: "Vancouver, BC", + }, + { + id: "3", + content: + "We're hiring! Looking for talented developers to join our team.", + author: "Bob Wilson", + location: "Calgary, Alberta", + }, + ]; + + demo.code("// Mock AI analysis (simulated):"); + demo.info("In a real scenario, this would call Ollama or OpenAI API"); + + mockPosts.forEach((post, index) => { + demo.info(`Post ${index + 1}: ${post.content.substring(0, 50)}...`); + demo.success( + `Analysis: Relevant to job layoffs (confidence: 0.${85 + index * 5})` + ); + console.log(); + }); + + demo.code("// Batch analysis simulation:"); + demo.info("Processing batch of 3 posts..."); + await simulateProcessing(); + demo.success("Batch analysis completed!"); + + await waitForEnter(); +} + +async function demonstrateIntegration() { + demo.section("5. Integration Example"); + demo.info("Here's how all utilities work together in a real scenario:"); + + const samplePost = { + id: "demo-1", + content: + "Just got #laidoff from my job at TechCorp! Looking for new opportunities in #Toronto. This is really tough but I'm staying positive! šŸš€", + author: "Demo User", + location: "Toronto, Ontario, Canada", + }; + + demo.code("// Processing pipeline:"); + + // 1. Log the start + logger.step("Processing new post"); + + // 2. Clean the text + const cleanedContent = cleanText(samplePost.content); + logger.info(`Cleaned content: ${cleanedContent}`); + + // 3. Check for keywords + const keywords = ["layoff", "downsizing", "RIF"]; + const hasKeywords = containsAnyKeyword(cleanedContent, keywords); + logger.search(`Keyword match: ${hasKeywords ? "Found" : "Not found"}`); + + // 4. Validate location + const locationFilters = parseLocationFilters("Ontario,Manitoba"); + const isValidLocation = validateLocationAgainstFilters( + samplePost.location, + locationFilters + ); + logger.location(`Location valid: ${isValidLocation ? "Yes" : "No"}`); + + // 5. Simulate AI analysis + if (hasKeywords && isValidLocation) { + logger.ai("Running AI analysis..."); + await simulateProcessing(); + logger.success("Post accepted and analyzed!"); + } else { + logger.warning("Post rejected - doesn't meet criteria"); + } + + await waitForEnter(); +} + +// Helper functions +function waitForEnter() { + return new Promise((resolve) => { + const readline = require("readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.question("\nPress Enter to continue...", () => { + rl.close(); + resolve(); + }); + }); +} + +async function simulateProcessing() { + return new Promise((resolve) => { + const dots = [".", "..", "..."]; + let i = 0; + const interval = setInterval(() => { + process.stdout.write(`\rProcessing${dots[i]}`); + i = (i + 1) % dots.length; + }, 500); + + setTimeout(() => { + clearInterval(interval); + process.stdout.write("\r"); + resolve(); + }, 2000); + }); +} + +// Run the demo if this file is executed directly +if (require.main === module) { + runDemo().catch((error) => { + demo.error(`Demo failed: ${error.message}`); + process.exit(1); + }); +} + +module.exports = { runDemo }; diff --git a/ai-analyzer/index.js b/ai-analyzer/index.js index 438d020..a079d99 100644 --- a/ai-analyzer/index.js +++ b/ai-analyzer/index.js @@ -1,22 +1,22 @@ -/** - * ai-analyzer - Core utilities for parsers - * Main entry point that exports all modules - */ - -// Export all utilities with clean namespace -module.exports = { - // Logger utilities - ...require("./src/logger"), - - // AI analysis utilities - ...require("./src/ai-utils"), - - // Text processing utilities - ...require("./src/text-utils"), - - // Location validation utilities - ...require("./src/location-utils"), - - // Test utilities - ...require("./src/test-utils"), -}; +/** + * ai-analyzer - Core utilities for parsers + * Main entry point that exports all modules + */ + +// Export all utilities with clean namespace +module.exports = { + // Logger utilities + ...require("./src/logger"), + + // AI analysis utilities + ...require("./src/ai-utils"), + + // Text processing utilities + ...require("./src/text-utils"), + + // Location validation utilities + ...require("./src/location-utils"), + + // Test utilities + ...require("./src/test-utils"), +}; diff --git a/ai-analyzer/package-lock.json b/ai-analyzer/package-lock.json index db031b5..d67277b 100644 --- a/ai-analyzer/package-lock.json +++ b/ai-analyzer/package-lock.json @@ -1,3714 +1,3714 @@ -{ - "name": "ai-analyzer", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ai-analyzer", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "chalk": "^4.1.2", - "csv-parser": "^3.2.0", - "dotenv": "^17.0.0" - }, - "devDependencies": { - "jest": "^29.7.0" - }, - "peerDependencies": { - "playwright": "^1.53.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.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, - "license": "MIT", - "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.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", - "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/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", - "@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, - "license": "MIT", - "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-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, - "license": "MIT", - "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, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.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, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.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, - "license": "MIT", - "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.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "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, - "license": "ISC", - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "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/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/node": { - "version": "24.0.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", - "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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, - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "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, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "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, - "license": "MIT" - }, - "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, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", - "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" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^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/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", - "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" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "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/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT" - }, - "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, - "license": "MIT" - }, - "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, - "license": "MIT" - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.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, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csv-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", - "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==", - "license": "MIT", - "bin": { - "csv-parser": "bin/csv-parser" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dotenv": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", - "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.187", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "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, - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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, - "license": "ISC" - }, - "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/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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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, - "license": "ISC", - "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/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, - "license": "ISC" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "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, - "license": "MIT" - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "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, - "license": "ISC", - "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, - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "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, - "license": "BSD-3-Clause", - "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, - "license": "BSD-3-Clause", - "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-instrument/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "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, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "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, - "license": "BSD-3-Clause", - "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.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "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, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "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, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "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, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "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, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "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, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "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, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "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, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/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, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "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": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "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, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/playwright": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "playwright-core": "1.54.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "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, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "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, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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, - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.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, - "license": "BSD-3-Clause" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "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" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.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, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "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, - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} +{ + "name": "ai-analyzer", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-analyzer", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chalk": "^4.1.2", + "csv-parser": "^3.2.0", + "dotenv": "^17.0.0" + }, + "devDependencies": { + "jest": "^29.7.0" + }, + "peerDependencies": { + "playwright": "^1.53.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.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, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "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/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@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, + "license": "MIT", + "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-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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "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/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.0.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", + "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.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, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csv-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", + "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==", + "license": "MIT", + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.187", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", + "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC" + }, + "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/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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "ISC", + "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/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, + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "BSD-3-Clause", + "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.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/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, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "playwright-core": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.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, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/ai-analyzer/src/ai-utils.js b/ai-analyzer/src/ai-utils.js index 74ee3f5..e62633f 100644 --- a/ai-analyzer/src/ai-utils.js +++ b/ai-analyzer/src/ai-utils.js @@ -1,301 +1,301 @@ -const { logger } = require("./logger"); - -/** - * AI Analysis utilities for post processing with Ollama - * Extracted from ai-analyzer-local.js for reuse across parsers - */ - -/** - * Check if Ollama is running and the model is available - */ -async function checkOllamaStatus( - model = "mistral", - ollamaHost = "http://localhost:11434" -) { - try { - // Check if Ollama is running - const response = await fetch(`${ollamaHost}/api/tags`); - if (!response.ok) { - throw new Error(`Ollama not running on ${ollamaHost}`); - } - - const data = await response.json(); - const availableModels = data.models.map((m) => m.name); - - logger.ai("Ollama is running"); - logger.info( - `šŸ“¦ Available models: ${availableModels - .map((m) => m.split(":")[0]) - .join(", ")}` - ); - - // Check if requested model is available - const modelExists = availableModels.some((m) => m.startsWith(model)); - if (!modelExists) { - logger.error(`Model "${model}" not found`); - logger.error(`šŸ’” Install it with: ollama pull ${model}`); - logger.error( - `šŸ’” Or choose from: ${availableModels - .map((m) => m.split(":")[0]) - .join(", ")}` - ); - return false; - } - - logger.success(`Using model: ${model}`); - return true; - } catch (error) { - logger.error(`Error connecting to Ollama: ${error.message}`); - logger.error("šŸ’” Make sure Ollama is installed and running:"); - logger.error(" 1. Install: https://ollama.ai/"); - logger.error(" 2. Start: ollama serve"); - logger.error(` 3. Install model: ollama pull ${model}`); - return false; - } -} - -/** - * Analyze multiple posts using local Ollama - */ -async function analyzeBatch( - posts, - context, - model = "mistral", - ollamaHost = "http://localhost:11434" -) { - logger.ai(`Analyzing batch of ${posts.length} posts with ${model}...`); - - try { - const prompt = `You are an expert at analyzing LinkedIn posts for relevance to specific contexts. - -CONTEXT TO MATCH: "${context}" - -Analyze these ${ - posts.length - } LinkedIn posts and determine if each relates to the context above. - -POSTS: -${posts - .map( - (post, i) => ` -POST ${i + 1}: -"${post.text.substring(0, 400)}${post.text.length > 400 ? "..." : ""}" -` - ) - .join("")} - -For each post, provide: -- Is it relevant to "${context}"? (YES/NO) -- Confidence level (0.0 to 1.0) -- Brief reasoning - -Respond in this EXACT format for each post: -POST 1: YES/NO | 0.X | brief reason -POST 2: YES/NO | 0.X | brief reason -POST 3: YES/NO | 0.X | brief reason - -Examples: -- For layoff context: "laid off 50 employees" = YES | 0.9 | mentions layoffs -- For hiring context: "we're hiring developers" = YES | 0.8 | job posting -- Unrelated content = NO | 0.1 | not relevant to context`; - - const response = await fetch(`${ollamaHost}/api/generate`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: model, - prompt: prompt, - stream: false, - options: { - temperature: 0.3, - top_p: 0.9, - }, - }), - }); - - if (!response.ok) { - throw new Error( - `Ollama API error: ${response.status} ${response.statusText}` - ); - } - - const data = await response.json(); - const aiResponse = data.response.trim(); - - // Parse the response - const analyses = []; - const lines = aiResponse.split("\n").filter((line) => line.trim()); - - for (let i = 0; i < posts.length; i++) { - let analysis = { - postIndex: i + 1, - isRelevant: false, - confidence: 0.5, - reasoning: "Could not parse AI response", - }; - - // Look for lines that match "POST X:" pattern - const postPattern = new RegExp(`POST\\s*${i + 1}:?\\s*(.+)`, "i"); - - for (const line of lines) { - const match = line.match(postPattern); - if (match) { - const content = match[1].trim(); - - // Parse: YES/NO | 0.X | reasoning - const parts = content.split("|").map((p) => p.trim()); - - if (parts.length >= 3) { - analysis.isRelevant = parts[0].toUpperCase().includes("YES"); - analysis.confidence = Math.max( - 0, - Math.min(1, parseFloat(parts[1]) || 0.5) - ); - analysis.reasoning = parts[2] || "No reasoning provided"; - } else { - // Fallback parsing - analysis.isRelevant = - content.toUpperCase().includes("YES") || - content.toLowerCase().includes("relevant"); - analysis.confidence = 0.6; - analysis.reasoning = content.substring(0, 100); - } - break; - } - } - - analyses.push(analysis); - } - - // If we didn't get enough analyses, fill in defaults - while (analyses.length < posts.length) { - analyses.push({ - postIndex: analyses.length + 1, - isRelevant: false, - confidence: 0.3, - reasoning: "AI response parsing failed", - }); - } - - return analyses; - } catch (error) { - logger.error(`Error in batch AI analysis: ${error.message}`); - - // Fallback: mark all as relevant with low confidence - return posts.map((_, i) => ({ - postIndex: i + 1, - isRelevant: true, - confidence: 0.3, - reasoning: `Analysis failed: ${error.message}`, - })); - } -} - -/** - * Analyze a single post using local Ollama (fallback) - */ -async function analyzeSinglePost( - text, - context, - model = "mistral", - ollamaHost = "http://localhost:11434" -) { - const prompt = `Analyze this LinkedIn post for relevance to: "${context}" - -Post: "${text}" - -Is this post relevant to "${context}"? Provide: -1. YES or NO -2. Confidence (0.0 to 1.0) -3. Brief reason - -Format: YES/NO | 0.X | reason`; - - try { - const response = await fetch(`${ollamaHost}/api/generate`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: model, - prompt: prompt, - stream: false, - options: { - temperature: 0.3, - }, - }), - }); - - if (!response.ok) { - throw new Error(`Ollama API error: ${response.status}`); - } - - const data = await response.json(); - const aiResponse = data.response.trim(); - - // Parse response - const parts = aiResponse.split("|").map((p) => p.trim()); - - if (parts.length >= 3) { - return { - isRelevant: parts[0].toUpperCase().includes("YES"), - confidence: Math.max(0, Math.min(1, parseFloat(parts[1]) || 0.5)), - reasoning: parts[2], - }; - } else { - // Fallback parsing - return { - isRelevant: - aiResponse.toLowerCase().includes("yes") || - aiResponse.toLowerCase().includes("relevant"), - confidence: 0.6, - reasoning: aiResponse.substring(0, 100), - }; - } - } catch (error) { - return { - isRelevant: true, // Default to include on error - confidence: 0.3, - reasoning: `Analysis failed: ${error.message}`, - }; - } -} - -/** - * Find the most recent results file if none specified - */ -function findLatestResultsFile(resultsDir = "results") { - const fs = require("fs"); - const path = require("path"); - - if (!fs.existsSync(resultsDir)) { - throw new Error("Results directory not found. Run the scraper first."); - } - - const files = fs - .readdirSync(resultsDir) - .filter( - (f) => - (f.startsWith("results-") || f.startsWith("linkedin-results-")) && - f.endsWith(".json") && - !f.includes("-ai-") - ) - .sort() - .reverse(); - - if (files.length === 0) { - throw new Error("No results files found. Run the scraper first."); - } - - return path.join(resultsDir, files[0]); -} - -module.exports = { - checkOllamaStatus, - analyzeBatch, - analyzeSinglePost, - findLatestResultsFile, -}; +const { logger } = require("./logger"); + +/** + * AI Analysis utilities for post processing with Ollama + * Extracted from ai-analyzer-local.js for reuse across parsers + */ + +/** + * Check if Ollama is running and the model is available + */ +async function checkOllamaStatus( + model = "mistral", + ollamaHost = "http://localhost:11434" +) { + try { + // Check if Ollama is running + const response = await fetch(`${ollamaHost}/api/tags`); + if (!response.ok) { + throw new Error(`Ollama not running on ${ollamaHost}`); + } + + const data = await response.json(); + const availableModels = data.models.map((m) => m.name); + + logger.ai("Ollama is running"); + logger.info( + `šŸ“¦ Available models: ${availableModels + .map((m) => m.split(":")[0]) + .join(", ")}` + ); + + // Check if requested model is available + const modelExists = availableModels.some((m) => m.startsWith(model)); + if (!modelExists) { + logger.error(`Model "${model}" not found`); + logger.error(`šŸ’” Install it with: ollama pull ${model}`); + logger.error( + `šŸ’” Or choose from: ${availableModels + .map((m) => m.split(":")[0]) + .join(", ")}` + ); + return false; + } + + logger.success(`Using model: ${model}`); + return true; + } catch (error) { + logger.error(`Error connecting to Ollama: ${error.message}`); + logger.error("šŸ’” Make sure Ollama is installed and running:"); + logger.error(" 1. Install: https://ollama.ai/"); + logger.error(" 2. Start: ollama serve"); + logger.error(` 3. Install model: ollama pull ${model}`); + return false; + } +} + +/** + * Analyze multiple posts using local Ollama + */ +async function analyzeBatch( + posts, + context, + model = "mistral", + ollamaHost = "http://localhost:11434" +) { + logger.ai(`Analyzing batch of ${posts.length} posts with ${model}...`); + + try { + const prompt = `You are an expert at analyzing LinkedIn posts for relevance to specific contexts. + +CONTEXT TO MATCH: "${context}" + +Analyze these ${ + posts.length + } LinkedIn posts and determine if each relates to the context above. + +POSTS: +${posts + .map( + (post, i) => ` +POST ${i + 1}: +"${post.text.substring(0, 400)}${post.text.length > 400 ? "..." : ""}" +` + ) + .join("")} + +For each post, provide: +- Is it relevant to "${context}"? (YES/NO) +- Confidence level (0.0 to 1.0) +- Brief reasoning + +Respond in this EXACT format for each post: +POST 1: YES/NO | 0.X | brief reason +POST 2: YES/NO | 0.X | brief reason +POST 3: YES/NO | 0.X | brief reason + +Examples: +- For layoff context: "laid off 50 employees" = YES | 0.9 | mentions layoffs +- For hiring context: "we're hiring developers" = YES | 0.8 | job posting +- Unrelated content = NO | 0.1 | not relevant to context`; + + const response = await fetch(`${ollamaHost}/api/generate`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: model, + prompt: prompt, + stream: false, + options: { + temperature: 0.3, + top_p: 0.9, + }, + }), + }); + + if (!response.ok) { + throw new Error( + `Ollama API error: ${response.status} ${response.statusText}` + ); + } + + const data = await response.json(); + const aiResponse = data.response.trim(); + + // Parse the response + const analyses = []; + const lines = aiResponse.split("\n").filter((line) => line.trim()); + + for (let i = 0; i < posts.length; i++) { + let analysis = { + postIndex: i + 1, + isRelevant: false, + confidence: 0.5, + reasoning: "Could not parse AI response", + }; + + // Look for lines that match "POST X:" pattern + const postPattern = new RegExp(`POST\\s*${i + 1}:?\\s*(.+)`, "i"); + + for (const line of lines) { + const match = line.match(postPattern); + if (match) { + const content = match[1].trim(); + + // Parse: YES/NO | 0.X | reasoning + const parts = content.split("|").map((p) => p.trim()); + + if (parts.length >= 3) { + analysis.isRelevant = parts[0].toUpperCase().includes("YES"); + analysis.confidence = Math.max( + 0, + Math.min(1, parseFloat(parts[1]) || 0.5) + ); + analysis.reasoning = parts[2] || "No reasoning provided"; + } else { + // Fallback parsing + analysis.isRelevant = + content.toUpperCase().includes("YES") || + content.toLowerCase().includes("relevant"); + analysis.confidence = 0.6; + analysis.reasoning = content.substring(0, 100); + } + break; + } + } + + analyses.push(analysis); + } + + // If we didn't get enough analyses, fill in defaults + while (analyses.length < posts.length) { + analyses.push({ + postIndex: analyses.length + 1, + isRelevant: false, + confidence: 0.3, + reasoning: "AI response parsing failed", + }); + } + + return analyses; + } catch (error) { + logger.error(`Error in batch AI analysis: ${error.message}`); + + // Fallback: mark all as relevant with low confidence + return posts.map((_, i) => ({ + postIndex: i + 1, + isRelevant: true, + confidence: 0.3, + reasoning: `Analysis failed: ${error.message}`, + })); + } +} + +/** + * Analyze a single post using local Ollama (fallback) + */ +async function analyzeSinglePost( + text, + context, + model = "mistral", + ollamaHost = "http://localhost:11434" +) { + const prompt = `Analyze this LinkedIn post for relevance to: "${context}" + +Post: "${text}" + +Is this post relevant to "${context}"? Provide: +1. YES or NO +2. Confidence (0.0 to 1.0) +3. Brief reason + +Format: YES/NO | 0.X | reason`; + + try { + const response = await fetch(`${ollamaHost}/api/generate`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: model, + prompt: prompt, + stream: false, + options: { + temperature: 0.3, + }, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama API error: ${response.status}`); + } + + const data = await response.json(); + const aiResponse = data.response.trim(); + + // Parse response + const parts = aiResponse.split("|").map((p) => p.trim()); + + if (parts.length >= 3) { + return { + isRelevant: parts[0].toUpperCase().includes("YES"), + confidence: Math.max(0, Math.min(1, parseFloat(parts[1]) || 0.5)), + reasoning: parts[2], + }; + } else { + // Fallback parsing + return { + isRelevant: + aiResponse.toLowerCase().includes("yes") || + aiResponse.toLowerCase().includes("relevant"), + confidence: 0.6, + reasoning: aiResponse.substring(0, 100), + }; + } + } catch (error) { + return { + isRelevant: true, // Default to include on error + confidence: 0.3, + reasoning: `Analysis failed: ${error.message}`, + }; + } +} + +/** + * Find the most recent results file if none specified + */ +function findLatestResultsFile(resultsDir = "results") { + const fs = require("fs"); + const path = require("path"); + + if (!fs.existsSync(resultsDir)) { + throw new Error("Results directory not found. Run the scraper first."); + } + + const files = fs + .readdirSync(resultsDir) + .filter( + (f) => + (f.startsWith("results-") || f.startsWith("linkedin-results-")) && + f.endsWith(".json") && + !f.includes("-ai-") + ) + .sort() + .reverse(); + + if (files.length === 0) { + throw new Error("No results files found. Run the scraper first."); + } + + return path.join(resultsDir, files[0]); +} + +module.exports = { + checkOllamaStatus, + analyzeBatch, + analyzeSinglePost, + findLatestResultsFile, +}; diff --git a/ai-analyzer/src/location-utils.js b/ai-analyzer/src/location-utils.js index 7635e4d..259f535 100644 --- a/ai-analyzer/src/location-utils.js +++ b/ai-analyzer/src/location-utils.js @@ -1,1123 +1,1123 @@ -/** - * Enhanced Location Filtering Utilities - Improved Version - * - * These utilities provide: - * - Comprehensive city/province lookup for Canada - * - Fast O(1) city-to-province matching - * - Flexible location filter parsing and validation - * - Used by parsers for profile location validation - * - * USAGE (for developers): - * const { parseLocationFilters, validateLocationAgainstFilters, extractLocationFromProfile } = require('ai-analyzer'); - */ - -// Suppress D-Bus notification errors in WSL -process.env.NO_AT_BRIDGE = "1"; -process.env.DBUS_SESSION_BUS_ADDRESS = "/dev/null"; - -// Organized by province with comprehensive coverage -const CITIES_BY_PROVINCE = { - ontario: [ - // Greater Toronto Area - "toronto", - "mississauga", - "brampton", - "markham", - "vaughan", - "richmond hill", - "oakville", - "burlington", - "pickering", - "ajax", - "whitby", - "oshawa", - "milton", - "newmarket", - "aurora", - "georgina", - "king", - "whitchurch-stouffville", - "caledon", - "halton hills", - "clarington", - "scugog", - "uxbridge", - - // Southwestern Ontario - "london", - "windsor", - "kitchener", - "waterloo", - "cambridge", - "guelph", - "brantford", - "woodstock", - "stratford", - "sarnia", - "chatham", - "leamington", - "kingsville", - "amherstburg", - "tecumseh", - "lakeshore", - "essex", - "tilbury", - "st. thomas", - "ingersoll", - "tillsonburg", - "simcoe", - "delhi", - "port dover", - "welland", - "niagara falls", - "st. catharines", - "thorold", - "fort erie", - "grimsby", - "lincoln", - "pelham", - "wainfleet", - "west lincoln", - - // Central Ontario - "hamilton", - "barrie", - "orillia", - "midland", - "penetanguishene", - "collingwood", - "wasaga beach", - "blue mountains", - "clearview", - "springwater", - "innisfil", - "bradford west gwillimbury", - "essa", - "new tecumseth", - "adjala-tosorontio", - "mono", - "orangeville", - "shelburne", - "mulmur", - "amaranth", - "east garafraxa", - - // Eastern Ontario - "ottawa", - "gatineau", - "kingston", - "cornwall", - "pembroke", - "petawawa", - "deep river", - "arnprior", - "renfrew", - "carleton place", - "almonte", - "smiths falls", - "perth", - "brockville", - "prescott", - "iroquois", - "morrisburg", - "winchester", - "kemptville", - "merrickville-wolford", - "westport", - "gananoque", - "lansdowne", - "belleville", - "trenton", - "picton", - "napanee", - "deseronto", - "quinte west", - - // Northern Ontario - "sudbury", - "north bay", - "sault ste. marie", - "thunder bay", - "timmins", - "kirkland lake", - "cochrane", - "kapuskasing", - "hearst", - "iroquois falls", - "smooth rock falls", - "matheson", - "new liskeard", - "haileybury", - "cobalt", - "temiskaming shores", - "englehart", - "elliot lake", - "espanola", - "blind river", - "spanish", - "massey", - "thessalon", - "wawa", - "chapleau", - "white river", - "marathon", - "terrace bay", - "schreiber", - "nipigon", - "red rock", - "geraldton", - "longlac", - "beardmore", - "greenstone", - "ignace", - "dryden", - "kenora", - "fort frances", - "atikokan", - "rainy river", - "emo", - "sioux lookout", - "pickle lake", - "red lake", - - // Additional mid-size communities - "cobourg", - "port hope", - "peterborough", - "lindsay", - "fenelon falls", - "bobcaygeon", - "minden", - "haliburton", - "bancroft", - "barry's bay", - "huntsville", - "bracebridge", - "gravenhurst", - "parry sound", - "burk's falls", - "powassan", - "callander", - "sturgeon falls", - "west nipissing", - "french river", - "killarney", - "gore bay", - "little current", - "mindemoya", - "wikwemikong", - "m'chigeeng", - "aundeck omni kaning", - ], - - manitoba: [ - "winnipeg", - "brandon", - "steinbach", - "thompson", - "portage la prairie", - "winkler", - "selkirk", - "morden", - "dauphin", - "the pas", - "flin flon", - "swan river", - "neepawa", - "virden", - "souris", - "carman", - "stonewall", - "beausejour", - "gimli", - "arborg", - "teulon", - "ashern", - "eriksdale", - "fisher branch", - "riverton", - "winnipeg beach", - "dunnottar", - "altona", - "morris", - "emerson", - "killarney", - "boissevain", - "deloraine", - "melita", - "waskada", - "cartwright", - "crystal city", - "pilot mound", - "manitou", - "la riviere", - "glenboro", - "treherne", - "holland", - "hamiota", - "shoal lake", - "russell", - "roblin", - "grandview", - "minitonas", - "bowsman", - "birtle", - "rossburn", - "sandy lake", - ], - - "british columbia": [ - "vancouver", - "surrey", - "burnaby", - "richmond", - "abbotsford", - "coquitlam", - "langley", - "delta", - "north vancouver", - "west vancouver", - "new westminster", - "port coquitlam", - "maple ridge", - "white rock", - "pitt meadows", - "port moody", - "bowen island", - "anmore", - "belcarra", - "lions bay", - "victoria", - "saanich", - "esquimalt", - "oak bay", - "view royal", - "sidney", - "central saanich", - "north saanich", - "highlands", - "metchosin", - "sooke", - "colwood", - "langford", - "duncan", - "nanaimo", - "parksville", - "qualicum beach", - "courtenay", - "comox", - "campbell river", - "port alberni", - "tofino", - "ucluelet", - "kelowna", - "vernon", - "penticton", - "kamloops", - "salmon arm", - "revelstoke", - "golden", - "invermere", - "cranbrook", - "fernie", - "kimberley", - "nelson", - "castlegar", - "trail", - "rossland", - "grand forks", - "osoyoos", - "oliver", - "summerland", - "peachland", - "westbank", - "prince george", - "quesnel", - "williams lake", - "100 mile house", - "clinton", - "cache creek", - "ashcroft", - "merritt", - "princeton", - "hope", - "chilliwack", - "mission", - "harrison hot springs", - "agassiz", - "kent", - "fraser valley", - "squamish", - "whistler", - "pemberton", - "lillooet", - "lytton", - "prince rupert", - "terrace", - "kitimat", - "smithers", - "burns lake", - "vanderhoof", - "fort st. john", - "dawson creek", - "tumbler ridge", - "chetwynd", - "hudson's hope", - "fort nelson", - "fort st. james", - ], - - alberta: [ - "calgary", - "edmonton", - "red deer", - "lethbridge", - "medicine hat", - "grande prairie", - "airdrie", - "spruce grove", - "leduc", - "lloydminster", - "camrose", - "wetaskiwin", - "lacombe", - "ponoka", - "sylvan lake", - "blackfalds", - "innisfail", - "olds", - "didsbury", - "carstairs", - "cochrane", - "canmore", - "banff", - "okotoks", - "high river", - "strathmore", - "chestermere", - "drumheller", - "three hills", - "hanna", - "oyen", - "consort", - "provost", - "wainwright", - "vermilion", - "lloydminster", - "bonnyville", - "cold lake", - "st. paul", - "two hills", - "vegreville", - "mundare", - "lamont", - "bruderheim", - "morinville", - "legal", - "bon accord", - "gibbons", - "redwater", - "smoky lake", - "willingdon", - "andrew", - "chipman", - "fort saskatchewan", - "sherwood park", - "beaumont", - "devon", - "calmar", - "thorsby", - "warburg", - "breton", - "winfield", - "drayton valley", - "rocky mountain house", - "sundre", - "caroline", - "rimbey", - "bentley", - "blackfalds", - "penhold", - "bowden", - "eckville", - "rocky mountain house", - "sundre", - "olds", - "fort mcmurray", - "slave lake", - "high prairie", - "valleyview", - "fox creek", - "whitecourt", - "mayerthorpe", - "barrhead", - "westlock", - "athabasca", - "boyle", - "newbrook", - "wandering river", - "peace river", - "grimshaw", - "manning", - "fairview", - "high level", - "rainbow lake", - "zama city", - ], - - quebec: [ - "montreal", - "quebec city", - "laval", - "gatineau", - "longueuil", - "sherbrooke", - "saguenay", - "levis", - "trois-rivieres", - "terrebonne", - "saint-jean-sur-richelieu", - "repentigny", - "brossard", - "drummondville", - "saint-jerome", - "granby", - "blainville", - "saint-hyacinthe", - "shawinigan", - "dollard-des-ormeaux", - "rimouski", - "sorel-tracy", - "victoriaville", - "saint-eustache", - "vaudreuil-dorion", - "val-d'or", - "salaberry-de-valleyfield", - "sept-iles", - "rouyn-noranda", - "thetford mines", - "alma", - "joliette", - "saint-georges", - "baie-comeau", - "mascouche", - "beloeil", - "chateauguay", - "saint-constant", - "sainte-catherine", - "saint-bruno-de-montarville", - "boucherville", - "saint-lambert", - "candiac", - "la prairie", - "saint-basile-le-grand", - "carignan", - "chambly", - "saint-mathieu-de-beloeil", - ], - - saskatchewan: [ - "saskatoon", - "regina", - "prince albert", - "moose jaw", - "swift current", - "yorkton", - "north battleford", - "estevan", - "weyburn", - "lloydminster", - "martensville", - "warman", - "humboldt", - "kindersley", - "melville", - "tisdale", - "nipawin", - "melfort", - "unity", - "biggar", - "rosetown", - "outlook", - "davidson", - "watrous", - "lanigan", - "wynyard", - "foam lake", - "canora", - "preeceville", - "kamsack", - "roblin", - "hudson bay", - "carrot river", - "white fox", - "spiritwood", - "maidstone", - "lashburn", - "cut knife", - "wilkie", - "macklin", - "luseland", - "kerrobert", - "kindersley", - "eston", - "elrose", - "alsask", - "leader", - "maple creek", - "shaunavon", - "gull lake", - "cabri", - "kyle", - "rosetown", - "kindersley", - ], - - "nova scotia": [ - "halifax", - "dartmouth", - "sydney", - "truro", - "new glasgow", - "glace bay", - "yarmouth", - "bridgewater", - "kentville", - "amherst", - "new waterford", - "sydney mines", - "antigonish", - "stellarton", - "westville", - "pictou", - "digby", - "windsor", - "wolfville", - "middleton", - "annapolis royal", - "liverpool", - "shelburne", - "lockeport", - "lunenburg", - "mahone bay", - "chester", - "hubbards", - "tantallon", - "fall river", - "beaver bank", - "sackville", - "bedford", - "cole harbour", - "eastern passage", - "porters lake", - "musquodoboit harbour", - "sheet harbour", - "stewiacke", - "shubenacadie", - "elmsdale", - "enfield", - "lantz", - "milford", - "gay's river", - "mount uniacke", - "nine mile river", - ], - - "new brunswick": [ - "saint john", - "moncton", - "fredericton", - "dieppe", - "riverview", - "miramichi", - "edmundston", - "campbellton", - "bathurst", - "sackville", - "sussex", - "hampton", - "quispamsis", - "rothesay", - "grand bay-westfield", - "st. stephen", - "st. andrews", - "blacks harbour", - "grand manan", - "deer island", - "campobello island", - "woodstock", - "hartland", - "florenceville-bristol", - "perth-andover", - "grand falls", - "plaster rock", - "tobique first nation", - "nackawic", - "mcadam", - "harvey", - "chipman", - "minto", - "gagetown", - "oromocto", - "new maryland", - "hanwell", - "kingsclear", - "stanley", - "doaktown", - "blackville", - "renous", - "boiestown", - "caraquet", - "shippagan", - "tracadie", - "neguac", - "rogersville", - "rexton", - "richibucto", - "bouctouche", - "shediac", - "cap-pele", - "beaubassin-est", - ], - - "newfoundland and labrador": [ - "st. johns", - "mount pearl", - "corner brook", - "conception bay south", - "paradise", - "grand falls-windsor", - "happy valley-goose bay", - "gander", - "carbonear", - "stephenville", - "bay roberts", - "clarenville", - "marystown", - "deer lake", - "channel-port aux basques", - "labrador city", - "wabana", - "holyrood", - "portugal cove-st. philips", - "torbay", - "pouch cove", - "flatrock", - "logy bay-middle cove-outer cove", - "petty harbour-maddox cove", - "bauline", - "witless bay", - "ferryland", - "aquaforte", - "renews-cappahayden", - "trepassey", - "branch", - "placentia", - "come by chance", - "sunnyside", - "whitbourne", - "chapel arm", - "bluewater", - "norman's cove-long cove", - "heart's content", - "heart's delight-islington", - "cavendish", - "new melbourne", - "whiteway", - "trinity", - "bonavista", - ], - - "prince edward island": [ - "charlottetown", - "summerside", - "stratford", - "cornwall", - "montague", - "souris", - "kensington", - "alberton", - "tignish", - "o'leary", - "wellington", - "borden-carleton", - "murray river", - "georgetown", - "crapaud", - "breadalbane", - "hunter river", - "new london", - "cavendish", - "stanley bridge", - "rustico", - "brackley", - "winsloe", - "york", - "tea hill", - "miltonvale park", - "sherwood", - "warren grove", - "clyde river", - "bonshaw", - "vernon bridge", - "orwell", - "wood islands", - "belle river", - "murray harbour", - "little sands", - "gladstone", - "annandale", - "montague", - "brudenell", - "cardigan", - "launching", - "pooles corner", - "morell", - "st. peters", - "red point", - "lakeville", - "souris west", - ], - - "northwest territories": [ - "yellowknife", - "hay river", - "inuvik", - "fort simpson", - "fort smith", - "norman wells", - "iqaluit", - "rankin inlet", - "arviat", - "baker lake", - "cambridge bay", - "gjoa haven", - "kugluktuk", - "taloyoak", - "fort mcpherson", - "aklavik", - "tuktoyaktuk", - "paulatuk", - "sachs harbour", - "ulukhaktok", - "tsiigehtchic", - "fort good hope", - "colville lake", - "tulita", - "deline", - "wrigley", - "nahanni butte", - "jean marie river", - "kakisa", - "enterprise", - "fort resolution", - "lutselk'e", - "gameti", - "wekweeti", - "whati", - "behchoko", - ], - - yukon: [ - "whitehorse", - "dawson city", - "watson lake", - "haines junction", - "carmacks", - "mayo", - "faro", - "ross river", - "teslin", - "carcross", - "tagish", - "marsh lake", - "ibex valley", - "mount lorne", - "granger", - "takhini", - "fish lake", - "mendenhall", - "pelly crossing", - "stewart crossing", - "beaver creek", - "destruction bay", - "burwash landing", - "kluane lake", - "silver city", - "champagne", - "old crow", - "eagle plains", - "fort mcpherson", - ], - - nunavut: [ - "iqaluit", - "rankin inlet", - "arviat", - "baker lake", - "cambridge bay", - "gjoa haven", - "kugluktuk", - "taloyoak", - "kugaaruk", - "igloolik", - "hall beach", - "pond inlet", - "arctic bay", - "clyde river", - "pangnirtung", - "cape dorset", - "kimmirut", - "sanikiluaq", - "whale cove", - "chesterfield inlet", - "coral harbour", - "naujaat", - "igloolik", - "sanirajak", - "grise fiord", - "resolute", - "alert", - "eureka", - ], -}; - -// Create reverse lookup for faster searching -const CITY_TO_PROVINCE = {}; -for (const [province, cities] of Object.entries(CITIES_BY_PROVINCE)) { - for (const city of cities) { - CITY_TO_PROVINCE[city.toLowerCase()] = province.toLowerCase(); - } -} - -// Province name variations and abbreviations -const PROVINCE_VARIATIONS = { - ontario: ["ontario", "ont", "on"], - manitoba: ["manitoba", "man", "mb"], - "british columbia": ["british columbia", "bc", "b.c."], - alberta: ["alberta", "alta", "ab"], - quebec: ["quebec", "que", "qc", "quĆ©bec"], - saskatchewan: ["saskatchewan", "sask", "sk"], - "nova scotia": ["nova scotia", "ns", "n.s."], - "new brunswick": ["new brunswick", "nb", "n.b."], - "newfoundland and labrador": [ - "newfoundland and labrador", - "nl", - "n.l.", - "newfoundland", - "nfld", - ], - "prince edward island": ["prince edward island", "pei", "p.e.i."], - "northwest territories": ["northwest territories", "nt", "n.w.t.", "nwt"], - yukon: ["yukon", "yt", "y.t."], - nunavut: ["nunavut", "nu", "nvt"], -}; - -/** - * Parse location filters from environment variable - * Supports multiple formats: - * - Single: "Ontario" - * - Multiple: "Ontario,Manitoba" or "Ontario|Manitoba" - * - Mixed: "Toronto,Ontario,Vancouver" - */ -function parseLocationFilters(locationFilterString) { - if (!locationFilterString) return []; - - // Split by comma or pipe - const filters = locationFilterString - .split(/[,|]/) - .map((f) => f.trim().toLowerCase()); - return filters.filter((f) => f.length > 0); -} - -/** - * Enhanced location validation with comprehensive city coverage - * @param {string} userLocation - User's location from LinkedIn profile - * @param {string[]} locationFilters - Array of location filters - * @returns {Object} - {isValid: boolean, matchedFilter: string, reasoning: string} - */ -function validateLocationAgainstFilters(userLocation, locationFilters) { - if (!userLocation || locationFilters.length === 0) { - return { - isValid: true, - matchedFilter: null, - reasoning: "No filtering applied", - }; - } - - const normalizedLocation = userLocation.toLowerCase(); - - // Check each filter - for (const filter of locationFilters) { - const normalizedFilter = filter.toLowerCase(); - - // 1. Direct string match with word boundaries - const filterRegex = new RegExp( - `\\b${normalizedFilter.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, - "i" - ); - if (filterRegex.test(normalizedLocation)) { - return { - isValid: true, - matchedFilter: filter, - reasoning: `Direct match: "${normalizedFilter}" found in "${userLocation}"`, - }; - } - - // 2. Check if filter is a province - look for cities in that province - const provinceVariations = PROVINCE_VARIATIONS[normalizedFilter] || []; - for (const variation of provinceVariations) { - const variationRegex = new RegExp( - `\\b${variation.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, - "i" - ); - if (variationRegex.test(normalizedLocation)) { - return { - isValid: true, - matchedFilter: filter, - reasoning: `Province match: "${variation}" found in "${userLocation}"`, - }; - } - } - - // 3. Check if any city in the location maps to the filtered province - for (const [city, province] of Object.entries(CITY_TO_PROVINCE)) { - // Use word boundary regex to match city as a whole word - const cityRegex = new RegExp( - `\\b${city.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, - "i" - ); - if (cityRegex.test(normalizedLocation) && province === normalizedFilter) { - return { - isValid: true, - matchedFilter: filter, - reasoning: `City-to-province match: "${city}" maps to "${province}"`, - }; - } - } - - // 4. Check if filter is a city and maps to a province mentioned in location - const mappedProvince = CITY_TO_PROVINCE[normalizedFilter]; - if (mappedProvince) { - const provinceVariations = PROVINCE_VARIATIONS[mappedProvince] || []; - for (const variation of provinceVariations) { - const variationRegex = new RegExp( - `\\b${variation.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, - "i" - ); - if (variationRegex.test(normalizedLocation)) { - return { - isValid: true, - matchedFilter: filter, - reasoning: `Reverse city match: "${filter}" is in "${mappedProvince}" which matches location`, - }; - } - } - } - - // 5. Partial city name matching (for areas like "Greater Toronto Area") - const words = normalizedLocation.split(/[\s,.-]+/); - for (const word of words) { - if (word.length > 3) { - // Avoid matching short words - // Use word boundary regex to match word as a whole city name - const mappedProvince = CITY_TO_PROVINCE[word]; - if (mappedProvince === normalizedFilter) { - return { - isValid: true, - matchedFilter: filter, - reasoning: `Partial city match: "${word}" from "${userLocation}" maps to "${normalizedFilter}"`, - }; - } - } - } - } - - return { - isValid: false, - matchedFilter: null, - reasoning: `Location "${userLocation}" does not match any of: ${locationFilters.join( - ", " - )}`, - }; -} - -/** - * Extract location from LinkedIn profile with improved selectors - * @param {Object} page - Playwright page object - * @returns {Promise} - Extracted location or empty string - */ -async function extractLocationFromProfile(page) { - // Enhanced selectors for location information - const locationSelectors = [ - // Primary location selectors - ".text-body-small.inline.t-black--light.break-words", - ".pv-text-details__left-panel .text-body-small", - ".pb2.pv-text-details__left-panel", - ".text-body-small.inline", - '[data-field="location_details"]', - - // Additional selectors for different LinkedIn layouts - ".pv-text-details__left-panel-item", - ".pv-entity__location", - ".pv-top-card__location", - ".pv-top-card--list-bullet .pv-top-card--list-bullet-item", - ".artdeco-entity-lockup__subtitle", - - // Mobile/responsive selectors - ".profile-topcard__location", - ".profile-topcard__location-data", - ]; - - for (const selector of locationSelectors) { - try { - const elements = await page.$$(selector); - - for (const element of elements) { - const text = await element.textContent(); - if (text && text.trim()) { - const cleanText = text.trim(); - - // Accept locations with OR without commas - // Common patterns: "Toronto, ON", "Toronto", "Toronto, Ontario, Canada" - if ( - cleanText.length > 2 && - (cleanText.includes(",") || /^[a-zA-Z\s.-]+$/.test(cleanText)) && - !cleanText.toLowerCase().includes("connection") && - !cleanText.toLowerCase().includes("follower") && - !cleanText.toLowerCase().includes("experience") && - cleanText.length < 100 - ) { - return cleanText; - } - } - } - } catch (e) { - // Continue to next selector - } - } - - return ""; -} - -/** - * Get statistics about city coverage - */ -function getCoverageStats() { - const stats = {}; - for (const [province, cities] of Object.entries(CITIES_BY_PROVINCE)) { - stats[province] = cities.length; - } - stats.total = Object.keys(CITY_TO_PROVINCE).length; - return stats; -} - -module.exports = { - parseLocationFilters, - validateLocationAgainstFilters, - extractLocationFromProfile, - CITY_TO_PROVINCE, - CITIES_BY_PROVINCE, - PROVINCE_VARIATIONS, - getCoverageStats, -}; +/** + * Enhanced Location Filtering Utilities - Improved Version + * + * These utilities provide: + * - Comprehensive city/province lookup for Canada + * - Fast O(1) city-to-province matching + * - Flexible location filter parsing and validation + * - Used by parsers for profile location validation + * + * USAGE (for developers): + * const { parseLocationFilters, validateLocationAgainstFilters, extractLocationFromProfile } = require('ai-analyzer'); + */ + +// Suppress D-Bus notification errors in WSL +process.env.NO_AT_BRIDGE = "1"; +process.env.DBUS_SESSION_BUS_ADDRESS = "/dev/null"; + +// Organized by province with comprehensive coverage +const CITIES_BY_PROVINCE = { + ontario: [ + // Greater Toronto Area + "toronto", + "mississauga", + "brampton", + "markham", + "vaughan", + "richmond hill", + "oakville", + "burlington", + "pickering", + "ajax", + "whitby", + "oshawa", + "milton", + "newmarket", + "aurora", + "georgina", + "king", + "whitchurch-stouffville", + "caledon", + "halton hills", + "clarington", + "scugog", + "uxbridge", + + // Southwestern Ontario + "london", + "windsor", + "kitchener", + "waterloo", + "cambridge", + "guelph", + "brantford", + "woodstock", + "stratford", + "sarnia", + "chatham", + "leamington", + "kingsville", + "amherstburg", + "tecumseh", + "lakeshore", + "essex", + "tilbury", + "st. thomas", + "ingersoll", + "tillsonburg", + "simcoe", + "delhi", + "port dover", + "welland", + "niagara falls", + "st. catharines", + "thorold", + "fort erie", + "grimsby", + "lincoln", + "pelham", + "wainfleet", + "west lincoln", + + // Central Ontario + "hamilton", + "barrie", + "orillia", + "midland", + "penetanguishene", + "collingwood", + "wasaga beach", + "blue mountains", + "clearview", + "springwater", + "innisfil", + "bradford west gwillimbury", + "essa", + "new tecumseth", + "adjala-tosorontio", + "mono", + "orangeville", + "shelburne", + "mulmur", + "amaranth", + "east garafraxa", + + // Eastern Ontario + "ottawa", + "gatineau", + "kingston", + "cornwall", + "pembroke", + "petawawa", + "deep river", + "arnprior", + "renfrew", + "carleton place", + "almonte", + "smiths falls", + "perth", + "brockville", + "prescott", + "iroquois", + "morrisburg", + "winchester", + "kemptville", + "merrickville-wolford", + "westport", + "gananoque", + "lansdowne", + "belleville", + "trenton", + "picton", + "napanee", + "deseronto", + "quinte west", + + // Northern Ontario + "sudbury", + "north bay", + "sault ste. marie", + "thunder bay", + "timmins", + "kirkland lake", + "cochrane", + "kapuskasing", + "hearst", + "iroquois falls", + "smooth rock falls", + "matheson", + "new liskeard", + "haileybury", + "cobalt", + "temiskaming shores", + "englehart", + "elliot lake", + "espanola", + "blind river", + "spanish", + "massey", + "thessalon", + "wawa", + "chapleau", + "white river", + "marathon", + "terrace bay", + "schreiber", + "nipigon", + "red rock", + "geraldton", + "longlac", + "beardmore", + "greenstone", + "ignace", + "dryden", + "kenora", + "fort frances", + "atikokan", + "rainy river", + "emo", + "sioux lookout", + "pickle lake", + "red lake", + + // Additional mid-size communities + "cobourg", + "port hope", + "peterborough", + "lindsay", + "fenelon falls", + "bobcaygeon", + "minden", + "haliburton", + "bancroft", + "barry's bay", + "huntsville", + "bracebridge", + "gravenhurst", + "parry sound", + "burk's falls", + "powassan", + "callander", + "sturgeon falls", + "west nipissing", + "french river", + "killarney", + "gore bay", + "little current", + "mindemoya", + "wikwemikong", + "m'chigeeng", + "aundeck omni kaning", + ], + + manitoba: [ + "winnipeg", + "brandon", + "steinbach", + "thompson", + "portage la prairie", + "winkler", + "selkirk", + "morden", + "dauphin", + "the pas", + "flin flon", + "swan river", + "neepawa", + "virden", + "souris", + "carman", + "stonewall", + "beausejour", + "gimli", + "arborg", + "teulon", + "ashern", + "eriksdale", + "fisher branch", + "riverton", + "winnipeg beach", + "dunnottar", + "altona", + "morris", + "emerson", + "killarney", + "boissevain", + "deloraine", + "melita", + "waskada", + "cartwright", + "crystal city", + "pilot mound", + "manitou", + "la riviere", + "glenboro", + "treherne", + "holland", + "hamiota", + "shoal lake", + "russell", + "roblin", + "grandview", + "minitonas", + "bowsman", + "birtle", + "rossburn", + "sandy lake", + ], + + "british columbia": [ + "vancouver", + "surrey", + "burnaby", + "richmond", + "abbotsford", + "coquitlam", + "langley", + "delta", + "north vancouver", + "west vancouver", + "new westminster", + "port coquitlam", + "maple ridge", + "white rock", + "pitt meadows", + "port moody", + "bowen island", + "anmore", + "belcarra", + "lions bay", + "victoria", + "saanich", + "esquimalt", + "oak bay", + "view royal", + "sidney", + "central saanich", + "north saanich", + "highlands", + "metchosin", + "sooke", + "colwood", + "langford", + "duncan", + "nanaimo", + "parksville", + "qualicum beach", + "courtenay", + "comox", + "campbell river", + "port alberni", + "tofino", + "ucluelet", + "kelowna", + "vernon", + "penticton", + "kamloops", + "salmon arm", + "revelstoke", + "golden", + "invermere", + "cranbrook", + "fernie", + "kimberley", + "nelson", + "castlegar", + "trail", + "rossland", + "grand forks", + "osoyoos", + "oliver", + "summerland", + "peachland", + "westbank", + "prince george", + "quesnel", + "williams lake", + "100 mile house", + "clinton", + "cache creek", + "ashcroft", + "merritt", + "princeton", + "hope", + "chilliwack", + "mission", + "harrison hot springs", + "agassiz", + "kent", + "fraser valley", + "squamish", + "whistler", + "pemberton", + "lillooet", + "lytton", + "prince rupert", + "terrace", + "kitimat", + "smithers", + "burns lake", + "vanderhoof", + "fort st. john", + "dawson creek", + "tumbler ridge", + "chetwynd", + "hudson's hope", + "fort nelson", + "fort st. james", + ], + + alberta: [ + "calgary", + "edmonton", + "red deer", + "lethbridge", + "medicine hat", + "grande prairie", + "airdrie", + "spruce grove", + "leduc", + "lloydminster", + "camrose", + "wetaskiwin", + "lacombe", + "ponoka", + "sylvan lake", + "blackfalds", + "innisfail", + "olds", + "didsbury", + "carstairs", + "cochrane", + "canmore", + "banff", + "okotoks", + "high river", + "strathmore", + "chestermere", + "drumheller", + "three hills", + "hanna", + "oyen", + "consort", + "provost", + "wainwright", + "vermilion", + "lloydminster", + "bonnyville", + "cold lake", + "st. paul", + "two hills", + "vegreville", + "mundare", + "lamont", + "bruderheim", + "morinville", + "legal", + "bon accord", + "gibbons", + "redwater", + "smoky lake", + "willingdon", + "andrew", + "chipman", + "fort saskatchewan", + "sherwood park", + "beaumont", + "devon", + "calmar", + "thorsby", + "warburg", + "breton", + "winfield", + "drayton valley", + "rocky mountain house", + "sundre", + "caroline", + "rimbey", + "bentley", + "blackfalds", + "penhold", + "bowden", + "eckville", + "rocky mountain house", + "sundre", + "olds", + "fort mcmurray", + "slave lake", + "high prairie", + "valleyview", + "fox creek", + "whitecourt", + "mayerthorpe", + "barrhead", + "westlock", + "athabasca", + "boyle", + "newbrook", + "wandering river", + "peace river", + "grimshaw", + "manning", + "fairview", + "high level", + "rainbow lake", + "zama city", + ], + + quebec: [ + "montreal", + "quebec city", + "laval", + "gatineau", + "longueuil", + "sherbrooke", + "saguenay", + "levis", + "trois-rivieres", + "terrebonne", + "saint-jean-sur-richelieu", + "repentigny", + "brossard", + "drummondville", + "saint-jerome", + "granby", + "blainville", + "saint-hyacinthe", + "shawinigan", + "dollard-des-ormeaux", + "rimouski", + "sorel-tracy", + "victoriaville", + "saint-eustache", + "vaudreuil-dorion", + "val-d'or", + "salaberry-de-valleyfield", + "sept-iles", + "rouyn-noranda", + "thetford mines", + "alma", + "joliette", + "saint-georges", + "baie-comeau", + "mascouche", + "beloeil", + "chateauguay", + "saint-constant", + "sainte-catherine", + "saint-bruno-de-montarville", + "boucherville", + "saint-lambert", + "candiac", + "la prairie", + "saint-basile-le-grand", + "carignan", + "chambly", + "saint-mathieu-de-beloeil", + ], + + saskatchewan: [ + "saskatoon", + "regina", + "prince albert", + "moose jaw", + "swift current", + "yorkton", + "north battleford", + "estevan", + "weyburn", + "lloydminster", + "martensville", + "warman", + "humboldt", + "kindersley", + "melville", + "tisdale", + "nipawin", + "melfort", + "unity", + "biggar", + "rosetown", + "outlook", + "davidson", + "watrous", + "lanigan", + "wynyard", + "foam lake", + "canora", + "preeceville", + "kamsack", + "roblin", + "hudson bay", + "carrot river", + "white fox", + "spiritwood", + "maidstone", + "lashburn", + "cut knife", + "wilkie", + "macklin", + "luseland", + "kerrobert", + "kindersley", + "eston", + "elrose", + "alsask", + "leader", + "maple creek", + "shaunavon", + "gull lake", + "cabri", + "kyle", + "rosetown", + "kindersley", + ], + + "nova scotia": [ + "halifax", + "dartmouth", + "sydney", + "truro", + "new glasgow", + "glace bay", + "yarmouth", + "bridgewater", + "kentville", + "amherst", + "new waterford", + "sydney mines", + "antigonish", + "stellarton", + "westville", + "pictou", + "digby", + "windsor", + "wolfville", + "middleton", + "annapolis royal", + "liverpool", + "shelburne", + "lockeport", + "lunenburg", + "mahone bay", + "chester", + "hubbards", + "tantallon", + "fall river", + "beaver bank", + "sackville", + "bedford", + "cole harbour", + "eastern passage", + "porters lake", + "musquodoboit harbour", + "sheet harbour", + "stewiacke", + "shubenacadie", + "elmsdale", + "enfield", + "lantz", + "milford", + "gay's river", + "mount uniacke", + "nine mile river", + ], + + "new brunswick": [ + "saint john", + "moncton", + "fredericton", + "dieppe", + "riverview", + "miramichi", + "edmundston", + "campbellton", + "bathurst", + "sackville", + "sussex", + "hampton", + "quispamsis", + "rothesay", + "grand bay-westfield", + "st. stephen", + "st. andrews", + "blacks harbour", + "grand manan", + "deer island", + "campobello island", + "woodstock", + "hartland", + "florenceville-bristol", + "perth-andover", + "grand falls", + "plaster rock", + "tobique first nation", + "nackawic", + "mcadam", + "harvey", + "chipman", + "minto", + "gagetown", + "oromocto", + "new maryland", + "hanwell", + "kingsclear", + "stanley", + "doaktown", + "blackville", + "renous", + "boiestown", + "caraquet", + "shippagan", + "tracadie", + "neguac", + "rogersville", + "rexton", + "richibucto", + "bouctouche", + "shediac", + "cap-pele", + "beaubassin-est", + ], + + "newfoundland and labrador": [ + "st. johns", + "mount pearl", + "corner brook", + "conception bay south", + "paradise", + "grand falls-windsor", + "happy valley-goose bay", + "gander", + "carbonear", + "stephenville", + "bay roberts", + "clarenville", + "marystown", + "deer lake", + "channel-port aux basques", + "labrador city", + "wabana", + "holyrood", + "portugal cove-st. philips", + "torbay", + "pouch cove", + "flatrock", + "logy bay-middle cove-outer cove", + "petty harbour-maddox cove", + "bauline", + "witless bay", + "ferryland", + "aquaforte", + "renews-cappahayden", + "trepassey", + "branch", + "placentia", + "come by chance", + "sunnyside", + "whitbourne", + "chapel arm", + "bluewater", + "norman's cove-long cove", + "heart's content", + "heart's delight-islington", + "cavendish", + "new melbourne", + "whiteway", + "trinity", + "bonavista", + ], + + "prince edward island": [ + "charlottetown", + "summerside", + "stratford", + "cornwall", + "montague", + "souris", + "kensington", + "alberton", + "tignish", + "o'leary", + "wellington", + "borden-carleton", + "murray river", + "georgetown", + "crapaud", + "breadalbane", + "hunter river", + "new london", + "cavendish", + "stanley bridge", + "rustico", + "brackley", + "winsloe", + "york", + "tea hill", + "miltonvale park", + "sherwood", + "warren grove", + "clyde river", + "bonshaw", + "vernon bridge", + "orwell", + "wood islands", + "belle river", + "murray harbour", + "little sands", + "gladstone", + "annandale", + "montague", + "brudenell", + "cardigan", + "launching", + "pooles corner", + "morell", + "st. peters", + "red point", + "lakeville", + "souris west", + ], + + "northwest territories": [ + "yellowknife", + "hay river", + "inuvik", + "fort simpson", + "fort smith", + "norman wells", + "iqaluit", + "rankin inlet", + "arviat", + "baker lake", + "cambridge bay", + "gjoa haven", + "kugluktuk", + "taloyoak", + "fort mcpherson", + "aklavik", + "tuktoyaktuk", + "paulatuk", + "sachs harbour", + "ulukhaktok", + "tsiigehtchic", + "fort good hope", + "colville lake", + "tulita", + "deline", + "wrigley", + "nahanni butte", + "jean marie river", + "kakisa", + "enterprise", + "fort resolution", + "lutselk'e", + "gameti", + "wekweeti", + "whati", + "behchoko", + ], + + yukon: [ + "whitehorse", + "dawson city", + "watson lake", + "haines junction", + "carmacks", + "mayo", + "faro", + "ross river", + "teslin", + "carcross", + "tagish", + "marsh lake", + "ibex valley", + "mount lorne", + "granger", + "takhini", + "fish lake", + "mendenhall", + "pelly crossing", + "stewart crossing", + "beaver creek", + "destruction bay", + "burwash landing", + "kluane lake", + "silver city", + "champagne", + "old crow", + "eagle plains", + "fort mcpherson", + ], + + nunavut: [ + "iqaluit", + "rankin inlet", + "arviat", + "baker lake", + "cambridge bay", + "gjoa haven", + "kugluktuk", + "taloyoak", + "kugaaruk", + "igloolik", + "hall beach", + "pond inlet", + "arctic bay", + "clyde river", + "pangnirtung", + "cape dorset", + "kimmirut", + "sanikiluaq", + "whale cove", + "chesterfield inlet", + "coral harbour", + "naujaat", + "igloolik", + "sanirajak", + "grise fiord", + "resolute", + "alert", + "eureka", + ], +}; + +// Create reverse lookup for faster searching +const CITY_TO_PROVINCE = {}; +for (const [province, cities] of Object.entries(CITIES_BY_PROVINCE)) { + for (const city of cities) { + CITY_TO_PROVINCE[city.toLowerCase()] = province.toLowerCase(); + } +} + +// Province name variations and abbreviations +const PROVINCE_VARIATIONS = { + ontario: ["ontario", "ont", "on"], + manitoba: ["manitoba", "man", "mb"], + "british columbia": ["british columbia", "bc", "b.c."], + alberta: ["alberta", "alta", "ab"], + quebec: ["quebec", "que", "qc", "quĆ©bec"], + saskatchewan: ["saskatchewan", "sask", "sk"], + "nova scotia": ["nova scotia", "ns", "n.s."], + "new brunswick": ["new brunswick", "nb", "n.b."], + "newfoundland and labrador": [ + "newfoundland and labrador", + "nl", + "n.l.", + "newfoundland", + "nfld", + ], + "prince edward island": ["prince edward island", "pei", "p.e.i."], + "northwest territories": ["northwest territories", "nt", "n.w.t.", "nwt"], + yukon: ["yukon", "yt", "y.t."], + nunavut: ["nunavut", "nu", "nvt"], +}; + +/** + * Parse location filters from environment variable + * Supports multiple formats: + * - Single: "Ontario" + * - Multiple: "Ontario,Manitoba" or "Ontario|Manitoba" + * - Mixed: "Toronto,Ontario,Vancouver" + */ +function parseLocationFilters(locationFilterString) { + if (!locationFilterString) return []; + + // Split by comma or pipe + const filters = locationFilterString + .split(/[,|]/) + .map((f) => f.trim().toLowerCase()); + return filters.filter((f) => f.length > 0); +} + +/** + * Enhanced location validation with comprehensive city coverage + * @param {string} userLocation - User's location from LinkedIn profile + * @param {string[]} locationFilters - Array of location filters + * @returns {Object} - {isValid: boolean, matchedFilter: string, reasoning: string} + */ +function validateLocationAgainstFilters(userLocation, locationFilters) { + if (!userLocation || locationFilters.length === 0) { + return { + isValid: true, + matchedFilter: null, + reasoning: "No filtering applied", + }; + } + + const normalizedLocation = userLocation.toLowerCase(); + + // Check each filter + for (const filter of locationFilters) { + const normalizedFilter = filter.toLowerCase(); + + // 1. Direct string match with word boundaries + const filterRegex = new RegExp( + `\\b${normalizedFilter.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, + "i" + ); + if (filterRegex.test(normalizedLocation)) { + return { + isValid: true, + matchedFilter: filter, + reasoning: `Direct match: "${normalizedFilter}" found in "${userLocation}"`, + }; + } + + // 2. Check if filter is a province - look for cities in that province + const provinceVariations = PROVINCE_VARIATIONS[normalizedFilter] || []; + for (const variation of provinceVariations) { + const variationRegex = new RegExp( + `\\b${variation.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, + "i" + ); + if (variationRegex.test(normalizedLocation)) { + return { + isValid: true, + matchedFilter: filter, + reasoning: `Province match: "${variation}" found in "${userLocation}"`, + }; + } + } + + // 3. Check if any city in the location maps to the filtered province + for (const [city, province] of Object.entries(CITY_TO_PROVINCE)) { + // Use word boundary regex to match city as a whole word + const cityRegex = new RegExp( + `\\b${city.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, + "i" + ); + if (cityRegex.test(normalizedLocation) && province === normalizedFilter) { + return { + isValid: true, + matchedFilter: filter, + reasoning: `City-to-province match: "${city}" maps to "${province}"`, + }; + } + } + + // 4. Check if filter is a city and maps to a province mentioned in location + const mappedProvince = CITY_TO_PROVINCE[normalizedFilter]; + if (mappedProvince) { + const provinceVariations = PROVINCE_VARIATIONS[mappedProvince] || []; + for (const variation of provinceVariations) { + const variationRegex = new RegExp( + `\\b${variation.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, + "i" + ); + if (variationRegex.test(normalizedLocation)) { + return { + isValid: true, + matchedFilter: filter, + reasoning: `Reverse city match: "${filter}" is in "${mappedProvince}" which matches location`, + }; + } + } + } + + // 5. Partial city name matching (for areas like "Greater Toronto Area") + const words = normalizedLocation.split(/[\s,.-]+/); + for (const word of words) { + if (word.length > 3) { + // Avoid matching short words + // Use word boundary regex to match word as a whole city name + const mappedProvince = CITY_TO_PROVINCE[word]; + if (mappedProvince === normalizedFilter) { + return { + isValid: true, + matchedFilter: filter, + reasoning: `Partial city match: "${word}" from "${userLocation}" maps to "${normalizedFilter}"`, + }; + } + } + } + } + + return { + isValid: false, + matchedFilter: null, + reasoning: `Location "${userLocation}" does not match any of: ${locationFilters.join( + ", " + )}`, + }; +} + +/** + * Extract location from LinkedIn profile with improved selectors + * @param {Object} page - Playwright page object + * @returns {Promise} - Extracted location or empty string + */ +async function extractLocationFromProfile(page) { + // Enhanced selectors for location information + const locationSelectors = [ + // Primary location selectors + ".text-body-small.inline.t-black--light.break-words", + ".pv-text-details__left-panel .text-body-small", + ".pb2.pv-text-details__left-panel", + ".text-body-small.inline", + '[data-field="location_details"]', + + // Additional selectors for different LinkedIn layouts + ".pv-text-details__left-panel-item", + ".pv-entity__location", + ".pv-top-card__location", + ".pv-top-card--list-bullet .pv-top-card--list-bullet-item", + ".artdeco-entity-lockup__subtitle", + + // Mobile/responsive selectors + ".profile-topcard__location", + ".profile-topcard__location-data", + ]; + + for (const selector of locationSelectors) { + try { + const elements = await page.$$(selector); + + for (const element of elements) { + const text = await element.textContent(); + if (text && text.trim()) { + const cleanText = text.trim(); + + // Accept locations with OR without commas + // Common patterns: "Toronto, ON", "Toronto", "Toronto, Ontario, Canada" + if ( + cleanText.length > 2 && + (cleanText.includes(",") || /^[a-zA-Z\s.-]+$/.test(cleanText)) && + !cleanText.toLowerCase().includes("connection") && + !cleanText.toLowerCase().includes("follower") && + !cleanText.toLowerCase().includes("experience") && + cleanText.length < 100 + ) { + return cleanText; + } + } + } + } catch (e) { + // Continue to next selector + } + } + + return ""; +} + +/** + * Get statistics about city coverage + */ +function getCoverageStats() { + const stats = {}; + for (const [province, cities] of Object.entries(CITIES_BY_PROVINCE)) { + stats[province] = cities.length; + } + stats.total = Object.keys(CITY_TO_PROVINCE).length; + return stats; +} + +module.exports = { + parseLocationFilters, + validateLocationAgainstFilters, + extractLocationFromProfile, + CITY_TO_PROVINCE, + CITIES_BY_PROVINCE, + PROVINCE_VARIATIONS, + getCoverageStats, +}; diff --git a/ai-analyzer/src/logger.js b/ai-analyzer/src/logger.js index d6f80df..d3c49f5 100644 --- a/ai-analyzer/src/logger.js +++ b/ai-analyzer/src/logger.js @@ -1,123 +1,123 @@ -const chalk = require("chalk"); - -/** - * Configurable logger with color support and level controls - * Can enable/disable different log levels: debug, info, warning, error, success - */ -class Logger { - constructor(options = {}) { - this.levels = { - debug: options.debug !== false, - info: options.info !== false, - warning: options.warning !== false, - error: options.error !== false, - success: options.success !== false, - }; - this.colors = options.colors !== false; - } - - _formatMessage(level, message, prefix = "") { - const timestamp = new Date().toLocaleTimeString(); - const fullMessage = `${prefix}${message}`; - - if (!this.colors) { - return `[${timestamp}] [${level.toUpperCase()}] ${fullMessage}`; - } - - switch (level) { - case "debug": - return chalk.gray(`[${timestamp}] [DEBUG] ${fullMessage}`); - case "info": - return chalk.blue(`[${timestamp}] [INFO] ${fullMessage}`); - case "warning": - return chalk.yellow(`[${timestamp}] [WARNING] ${fullMessage}`); - case "error": - return chalk.red(`[${timestamp}] [ERROR] ${fullMessage}`); - case "success": - return chalk.green(`[${timestamp}] [SUCCESS] ${fullMessage}`); - default: - return `[${timestamp}] [${level.toUpperCase()}] ${fullMessage}`; - } - } - - debug(message) { - if (this.levels.debug) { - console.log(this._formatMessage("debug", message)); - } - } - - info(message) { - if (this.levels.info) { - console.log(this._formatMessage("info", message)); - } - } - - warning(message) { - if (this.levels.warning) { - console.warn(this._formatMessage("warning", message)); - } - } - - error(message) { - if (this.levels.error) { - console.error(this._formatMessage("error", message)); - } - } - - success(message) { - if (this.levels.success) { - console.log(this._formatMessage("success", message)); - } - } - - // Convenience methods with emoji prefixes for better UX - step(message) { - this.info(`šŸš€ ${message}`); - } - - search(message) { - this.info(`šŸ” ${message}`); - } - - ai(message) { - this.info(`🧠 ${message}`); - } - - location(message) { - this.info(`šŸ“ ${message}`); - } - - file(message) { - this.info(`šŸ“„ ${message}`); - } - - // Configure logger levels at runtime - setLevel(level, enabled) { - if (this.levels.hasOwnProperty(level)) { - this.levels[level] = enabled; - } - } - - // Disable all logging - silent() { - Object.keys(this.levels).forEach((level) => { - this.levels[level] = false; - }); - } - - // Enable all logging - verbose() { - Object.keys(this.levels).forEach((level) => { - this.levels[level] = true; - }); - } -} - -// Create default logger instance -const logger = new Logger(); - -// Export both the class and default instance -module.exports = { - Logger, - logger, -}; +const chalk = require("chalk"); + +/** + * Configurable logger with color support and level controls + * Can enable/disable different log levels: debug, info, warning, error, success + */ +class Logger { + constructor(options = {}) { + this.levels = { + debug: options.debug !== false, + info: options.info !== false, + warning: options.warning !== false, + error: options.error !== false, + success: options.success !== false, + }; + this.colors = options.colors !== false; + } + + _formatMessage(level, message, prefix = "") { + const timestamp = new Date().toLocaleTimeString(); + const fullMessage = `${prefix}${message}`; + + if (!this.colors) { + return `[${timestamp}] [${level.toUpperCase()}] ${fullMessage}`; + } + + switch (level) { + case "debug": + return chalk.gray(`[${timestamp}] [DEBUG] ${fullMessage}`); + case "info": + return chalk.blue(`[${timestamp}] [INFO] ${fullMessage}`); + case "warning": + return chalk.yellow(`[${timestamp}] [WARNING] ${fullMessage}`); + case "error": + return chalk.red(`[${timestamp}] [ERROR] ${fullMessage}`); + case "success": + return chalk.green(`[${timestamp}] [SUCCESS] ${fullMessage}`); + default: + return `[${timestamp}] [${level.toUpperCase()}] ${fullMessage}`; + } + } + + debug(message) { + if (this.levels.debug) { + console.log(this._formatMessage("debug", message)); + } + } + + info(message) { + if (this.levels.info) { + console.log(this._formatMessage("info", message)); + } + } + + warning(message) { + if (this.levels.warning) { + console.warn(this._formatMessage("warning", message)); + } + } + + error(message) { + if (this.levels.error) { + console.error(this._formatMessage("error", message)); + } + } + + success(message) { + if (this.levels.success) { + console.log(this._formatMessage("success", message)); + } + } + + // Convenience methods with emoji prefixes for better UX + step(message) { + this.info(`šŸš€ ${message}`); + } + + search(message) { + this.info(`šŸ” ${message}`); + } + + ai(message) { + this.info(`🧠 ${message}`); + } + + location(message) { + this.info(`šŸ“ ${message}`); + } + + file(message) { + this.info(`šŸ“„ ${message}`); + } + + // Configure logger levels at runtime + setLevel(level, enabled) { + if (this.levels.hasOwnProperty(level)) { + this.levels[level] = enabled; + } + } + + // Disable all logging + silent() { + Object.keys(this.levels).forEach((level) => { + this.levels[level] = false; + }); + } + + // Enable all logging + verbose() { + Object.keys(this.levels).forEach((level) => { + this.levels[level] = true; + }); + } +} + +// Create default logger instance +const logger = new Logger(); + +// Export both the class and default instance +module.exports = { + Logger, + logger, +}; diff --git a/ai-analyzer/src/test-utils.js b/ai-analyzer/src/test-utils.js index 8bc07b1..bdf40e2 100644 --- a/ai-analyzer/src/test-utils.js +++ b/ai-analyzer/src/test-utils.js @@ -1,124 +1,124 @@ -/** - * Shared test utilities for parsers - * Common mocks, helpers, and test data - */ - -/** - * Mock Playwright page object for testing - */ -function createMockPage() { - return { - goto: jest.fn().mockResolvedValue(undefined), - waitForSelector: jest.fn().mockResolvedValue(undefined), - $$: jest.fn().mockResolvedValue([]), - $: jest.fn().mockResolvedValue(null), - textContent: jest.fn().mockResolvedValue(""), - close: jest.fn().mockResolvedValue(undefined), - }; -} - -/** - * Mock fetch for AI API calls - */ -function createMockFetch(response = {}) { - return jest.fn().mockResolvedValue({ - ok: true, - status: 200, - json: jest.fn().mockResolvedValue(response), - ...response, - }); -} - -/** - * Sample test data for posts - */ -const samplePosts = [ - { - text: "We are laying off 100 employees due to economic downturn.", - keyword: "layoff", - profileLink: "https://linkedin.com/in/test-user-1", - }, - { - text: "Exciting opportunity! We are hiring senior developers for our team.", - keyword: "hiring", - profileLink: "https://linkedin.com/in/test-user-2", - }, -]; - -/** - * Sample location test data - */ -const sampleLocations = [ - "Toronto, Ontario, Canada", - "Vancouver, BC", - "Calgary, Alberta", - "Montreal, Quebec", - "Halifax, Nova Scotia", -]; - -/** - * Common test assertions - */ -function expectValidPost(post) { - expect(post).toHaveProperty("text"); - expect(post).toHaveProperty("keyword"); - expect(post).toHaveProperty("profileLink"); - expect(typeof post.text).toBe("string"); - expect(post.text.length).toBeGreaterThan(0); -} - -function expectValidAIAnalysis(analysis) { - expect(analysis).toHaveProperty("isRelevant"); - expect(analysis).toHaveProperty("confidence"); - expect(analysis).toHaveProperty("reasoning"); - expect(typeof analysis.isRelevant).toBe("boolean"); - expect(analysis.confidence).toBeGreaterThanOrEqual(0); - expect(analysis.confidence).toBeLessThanOrEqual(1); -} - -function expectValidLocation(location) { - expect(typeof location).toBe("string"); - expect(location.length).toBeGreaterThan(0); -} - -/** - * Test environment setup - */ -function setupTestEnv() { - // Mock environment variables - process.env.NODE_ENV = "test"; - process.env.OLLAMA_HOST = "http://localhost:11434"; - process.env.AI_CONTEXT = "test context"; - - // Suppress console output during tests - jest.spyOn(console, "log").mockImplementation(() => {}); - jest.spyOn(console, "error").mockImplementation(() => {}); - jest.spyOn(console, "warn").mockImplementation(() => {}); -} - -/** - * Clean up test environment - */ -function teardownTestEnv() { - // Restore console - console.log.mockRestore(); - console.error.mockRestore(); - console.warn.mockRestore(); - - // Clear environment - delete process.env.NODE_ENV; - delete process.env.OLLAMA_HOST; - delete process.env.AI_CONTEXT; -} - -module.exports = { - createMockPage, - createMockFetch, - samplePosts, - sampleLocations, - expectValidPost, - expectValidAIAnalysis, - expectValidLocation, - setupTestEnv, - teardownTestEnv, -}; +/** + * Shared test utilities for parsers + * Common mocks, helpers, and test data + */ + +/** + * Mock Playwright page object for testing + */ +function createMockPage() { + return { + goto: jest.fn().mockResolvedValue(undefined), + waitForSelector: jest.fn().mockResolvedValue(undefined), + $$: jest.fn().mockResolvedValue([]), + $: jest.fn().mockResolvedValue(null), + textContent: jest.fn().mockResolvedValue(""), + close: jest.fn().mockResolvedValue(undefined), + }; +} + +/** + * Mock fetch for AI API calls + */ +function createMockFetch(response = {}) { + return jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(response), + ...response, + }); +} + +/** + * Sample test data for posts + */ +const samplePosts = [ + { + text: "We are laying off 100 employees due to economic downturn.", + keyword: "layoff", + profileLink: "https://linkedin.com/in/test-user-1", + }, + { + text: "Exciting opportunity! We are hiring senior developers for our team.", + keyword: "hiring", + profileLink: "https://linkedin.com/in/test-user-2", + }, +]; + +/** + * Sample location test data + */ +const sampleLocations = [ + "Toronto, Ontario, Canada", + "Vancouver, BC", + "Calgary, Alberta", + "Montreal, Quebec", + "Halifax, Nova Scotia", +]; + +/** + * Common test assertions + */ +function expectValidPost(post) { + expect(post).toHaveProperty("text"); + expect(post).toHaveProperty("keyword"); + expect(post).toHaveProperty("profileLink"); + expect(typeof post.text).toBe("string"); + expect(post.text.length).toBeGreaterThan(0); +} + +function expectValidAIAnalysis(analysis) { + expect(analysis).toHaveProperty("isRelevant"); + expect(analysis).toHaveProperty("confidence"); + expect(analysis).toHaveProperty("reasoning"); + expect(typeof analysis.isRelevant).toBe("boolean"); + expect(analysis.confidence).toBeGreaterThanOrEqual(0); + expect(analysis.confidence).toBeLessThanOrEqual(1); +} + +function expectValidLocation(location) { + expect(typeof location).toBe("string"); + expect(location.length).toBeGreaterThan(0); +} + +/** + * Test environment setup + */ +function setupTestEnv() { + // Mock environment variables + process.env.NODE_ENV = "test"; + process.env.OLLAMA_HOST = "http://localhost:11434"; + process.env.AI_CONTEXT = "test context"; + + // Suppress console output during tests + jest.spyOn(console, "log").mockImplementation(() => {}); + jest.spyOn(console, "error").mockImplementation(() => {}); + jest.spyOn(console, "warn").mockImplementation(() => {}); +} + +/** + * Clean up test environment + */ +function teardownTestEnv() { + // Restore console + console.log.mockRestore(); + console.error.mockRestore(); + console.warn.mockRestore(); + + // Clear environment + delete process.env.NODE_ENV; + delete process.env.OLLAMA_HOST; + delete process.env.AI_CONTEXT; +} + +module.exports = { + createMockPage, + createMockFetch, + samplePosts, + sampleLocations, + expectValidPost, + expectValidAIAnalysis, + expectValidLocation, + setupTestEnv, + teardownTestEnv, +}; diff --git a/ai-analyzer/src/text-utils.js b/ai-analyzer/src/text-utils.js index c3fdc8d..7635741 100644 --- a/ai-analyzer/src/text-utils.js +++ b/ai-analyzer/src/text-utils.js @@ -1,107 +1,107 @@ -/** - * Text processing utilities for cleaning and validating content - * Extracted from linkedout.js for reuse across parsers - */ - -/** - * Clean text by removing hashtags, URLs, emojis, and normalizing whitespace - */ -function cleanText(text) { - if (!text || typeof text !== "string") { - return ""; - } - - // Remove hashtags - text = text.replace(/#\w+/g, ""); - - // Remove hashtag mentions - text = text.replace(/\bhashtag\b/gi, ""); - text = text.replace(/hashtag-\w+/gi, ""); - - // Remove URLs - text = text.replace(/https?:\/\/[^\s]+/g, ""); - - // Remove emojis (Unicode ranges for common emoji) - text = text.replace( - /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}]/gu, - "" - ); - - // Normalize whitespace - text = text.replace(/\s+/g, " ").trim(); - - return text; -} - -/** - * Check if text contains any of the specified keywords (case insensitive) - */ -function containsAnyKeyword(text, keywords) { - if (!text || !Array.isArray(keywords)) { - return false; - } - - const lowerText = text.toLowerCase(); - return keywords.some((keyword) => lowerText.includes(keyword.toLowerCase())); -} - -/** - * Validate if text meets basic quality criteria - */ -function isValidText(text, minLength = 30) { - if (!text || typeof text !== "string") { - return false; - } - - // Check minimum length - if (text.length < minLength) { - return false; - } - - // Check if text contains alphanumeric characters - if (!/[a-zA-Z0-9]/.test(text)) { - return false; - } - - return true; -} - -/** - * Extract domain from URL - */ -function extractDomain(url) { - if (!url || typeof url !== "string") { - return null; - } - - try { - const urlObj = new URL(url); - return urlObj.hostname; - } catch (error) { - return null; - } -} - -/** - * Normalize URL by removing query parameters and fragments - */ -function normalizeUrl(url) { - if (!url || typeof url !== "string") { - return ""; - } - - try { - const urlObj = new URL(url); - return `${urlObj.protocol}//${urlObj.hostname}${urlObj.pathname}`; - } catch (error) { - return url; - } -} - -module.exports = { - cleanText, - containsAnyKeyword, - isValidText, - extractDomain, - normalizeUrl, -}; +/** + * Text processing utilities for cleaning and validating content + * Extracted from linkedout.js for reuse across parsers + */ + +/** + * Clean text by removing hashtags, URLs, emojis, and normalizing whitespace + */ +function cleanText(text) { + if (!text || typeof text !== "string") { + return ""; + } + + // Remove hashtags + text = text.replace(/#\w+/g, ""); + + // Remove hashtag mentions + text = text.replace(/\bhashtag\b/gi, ""); + text = text.replace(/hashtag-\w+/gi, ""); + + // Remove URLs + text = text.replace(/https?:\/\/[^\s]+/g, ""); + + // Remove emojis (Unicode ranges for common emoji) + text = text.replace( + /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}]/gu, + "" + ); + + // Normalize whitespace + text = text.replace(/\s+/g, " ").trim(); + + return text; +} + +/** + * Check if text contains any of the specified keywords (case insensitive) + */ +function containsAnyKeyword(text, keywords) { + if (!text || !Array.isArray(keywords)) { + return false; + } + + const lowerText = text.toLowerCase(); + return keywords.some((keyword) => lowerText.includes(keyword.toLowerCase())); +} + +/** + * Validate if text meets basic quality criteria + */ +function isValidText(text, minLength = 30) { + if (!text || typeof text !== "string") { + return false; + } + + // Check minimum length + if (text.length < minLength) { + return false; + } + + // Check if text contains alphanumeric characters + if (!/[a-zA-Z0-9]/.test(text)) { + return false; + } + + return true; +} + +/** + * Extract domain from URL + */ +function extractDomain(url) { + if (!url || typeof url !== "string") { + return null; + } + + try { + const urlObj = new URL(url); + return urlObj.hostname; + } catch (error) { + return null; + } +} + +/** + * Normalize URL by removing query parameters and fragments + */ +function normalizeUrl(url) { + if (!url || typeof url !== "string") { + return ""; + } + + try { + const urlObj = new URL(url); + return `${urlObj.protocol}//${urlObj.hostname}${urlObj.pathname}`; + } catch (error) { + return url; + } +} + +module.exports = { + cleanText, + containsAnyKeyword, + isValidText, + extractDomain, + normalizeUrl, +}; diff --git a/ai-analyzer/test/logger.test.js b/ai-analyzer/test/logger.test.js index 85112d0..be8ca41 100644 --- a/ai-analyzer/test/logger.test.js +++ b/ai-analyzer/test/logger.test.js @@ -1,194 +1,194 @@ -/** - * Test file for logger functionality - */ - -const { Logger, logger } = require("../src/logger"); - -describe("Logger", () => { - let consoleSpy; - let consoleWarnSpy; - let consoleErrorSpy; - - beforeEach(() => { - consoleSpy = jest.spyOn(console, "log").mockImplementation(); - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); - consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); - }); - - afterEach(() => { - consoleSpy.mockRestore(); - consoleWarnSpy.mockRestore(); - consoleErrorSpy.mockRestore(); - }); - - test("should create default logger instance", () => { - expect(logger).toBeDefined(); - expect(logger).toBeInstanceOf(Logger); - }); - - test("should log info messages", () => { - logger.info("Test message"); - expect(consoleSpy).toHaveBeenCalled(); - }); - - test("should create custom logger with disabled levels", () => { - const customLogger = new Logger({ debug: false }); - customLogger.debug("This should not log"); - expect(consoleSpy).not.toHaveBeenCalled(); - }); - - test("should use emoji prefixes for convenience methods", () => { - logger.step("Test step"); - logger.ai("Test AI"); - logger.location("Test location"); - expect(consoleSpy).toHaveBeenCalledTimes(3); - }); - - test("should configure levels at runtime", () => { - const customLogger = new Logger(); - customLogger.setLevel("debug", false); - customLogger.debug("This should not log"); - expect(consoleSpy).not.toHaveBeenCalled(); - }); - - test("should go silent when requested", () => { - const customLogger = new Logger(); - customLogger.silent(); - customLogger.info("This should not log"); - customLogger.error("This should not log"); - expect(consoleSpy).not.toHaveBeenCalled(); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); - - // Additional test cases for comprehensive coverage - - test("should log warning messages", () => { - logger.warning("Test warning"); - expect(consoleWarnSpy).toHaveBeenCalled(); - }); - - test("should log error messages", () => { - logger.error("Test error"); - expect(consoleErrorSpy).toHaveBeenCalled(); - }); - - test("should log success messages", () => { - logger.success("Test success"); - expect(consoleSpy).toHaveBeenCalled(); - }); - - test("should log debug messages", () => { - logger.debug("Test debug"); - expect(consoleSpy).toHaveBeenCalled(); - }); - - test("should respect disabled warning level", () => { - const customLogger = new Logger({ warning: false }); - customLogger.warning("This should not log"); - expect(consoleWarnSpy).not.toHaveBeenCalled(); - }); - - test("should respect disabled error level", () => { - const customLogger = new Logger({ error: false }); - customLogger.error("This should not log"); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); - - test("should respect disabled success level", () => { - const customLogger = new Logger({ success: false }); - customLogger.success("This should not log"); - expect(consoleSpy).not.toHaveBeenCalled(); - }); - - test("should respect disabled info level", () => { - const customLogger = new Logger({ info: false }); - customLogger.info("This should not log"); - expect(consoleSpy).not.toHaveBeenCalled(); - }); - - test("should test all convenience methods", () => { - logger.step("Test step"); - logger.search("Test search"); - logger.ai("Test AI"); - logger.location("Test location"); - logger.file("Test file"); - expect(consoleSpy).toHaveBeenCalledTimes(5); - }); - - test("should enable all levels with verbose method", () => { - const customLogger = new Logger({ debug: false, info: false }); - customLogger.verbose(); - customLogger.debug("This should log"); - customLogger.info("This should log"); - expect(consoleSpy).toHaveBeenCalledTimes(2); - }); - - test("should handle setLevel with invalid level gracefully", () => { - const customLogger = new Logger(); - expect(() => { - customLogger.setLevel("invalid", false); - }).not.toThrow(); - }); - - test("should format messages with timestamps", () => { - logger.info("Test message"); - const loggedMessage = consoleSpy.mock.calls[0][0]; - expect(loggedMessage).toMatch(/\[\d{1,2}:\d{2}:\d{2}\]/); - }); - - test("should include level in formatted messages", () => { - logger.info("Test message"); - const loggedMessage = consoleSpy.mock.calls[0][0]; - expect(loggedMessage).toContain("[INFO]"); - }); - - test("should disable colors when colors option is false", () => { - const customLogger = new Logger({ colors: false }); - customLogger.info("Test message"); - const loggedMessage = consoleSpy.mock.calls[0][0]; - // Should not contain ANSI color codes - expect(loggedMessage).not.toMatch(/\u001b\[/); - }); - - test("should enable colors by default", () => { - logger.info("Test message"); - const loggedMessage = consoleSpy.mock.calls[0][0]; - // Should contain ANSI color codes - expect(loggedMessage).toMatch(/\u001b\[/); - }); - - test("should handle multiple level configurations", () => { - const customLogger = new Logger({ - debug: false, - info: true, - warning: false, - error: true, - success: false, - }); - - customLogger.debug("Should not log"); - customLogger.info("Should log"); - customLogger.warning("Should not log"); - customLogger.error("Should log"); - customLogger.success("Should not log"); - - expect(consoleSpy).toHaveBeenCalledTimes(1); - expect(consoleErrorSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).not.toHaveBeenCalled(); - }); - - test("should handle empty or undefined messages", () => { - expect(() => { - logger.info(""); - logger.info(undefined); - logger.info(null); - }).not.toThrow(); - }); - - test("should handle complex message objects", () => { - const testObj = { key: "value", nested: { data: "test" } }; - expect(() => { - logger.info(testObj); - }).not.toThrow(); - }); -}); +/** + * Test file for logger functionality + */ + +const { Logger, logger } = require("../src/logger"); + +describe("Logger", () => { + let consoleSpy; + let consoleWarnSpy; + let consoleErrorSpy; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, "log").mockImplementation(); + consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); + consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + + test("should create default logger instance", () => { + expect(logger).toBeDefined(); + expect(logger).toBeInstanceOf(Logger); + }); + + test("should log info messages", () => { + logger.info("Test message"); + expect(consoleSpy).toHaveBeenCalled(); + }); + + test("should create custom logger with disabled levels", () => { + const customLogger = new Logger({ debug: false }); + customLogger.debug("This should not log"); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + test("should use emoji prefixes for convenience methods", () => { + logger.step("Test step"); + logger.ai("Test AI"); + logger.location("Test location"); + expect(consoleSpy).toHaveBeenCalledTimes(3); + }); + + test("should configure levels at runtime", () => { + const customLogger = new Logger(); + customLogger.setLevel("debug", false); + customLogger.debug("This should not log"); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + test("should go silent when requested", () => { + const customLogger = new Logger(); + customLogger.silent(); + customLogger.info("This should not log"); + customLogger.error("This should not log"); + expect(consoleSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + // Additional test cases for comprehensive coverage + + test("should log warning messages", () => { + logger.warning("Test warning"); + expect(consoleWarnSpy).toHaveBeenCalled(); + }); + + test("should log error messages", () => { + logger.error("Test error"); + expect(consoleErrorSpy).toHaveBeenCalled(); + }); + + test("should log success messages", () => { + logger.success("Test success"); + expect(consoleSpy).toHaveBeenCalled(); + }); + + test("should log debug messages", () => { + logger.debug("Test debug"); + expect(consoleSpy).toHaveBeenCalled(); + }); + + test("should respect disabled warning level", () => { + const customLogger = new Logger({ warning: false }); + customLogger.warning("This should not log"); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + test("should respect disabled error level", () => { + const customLogger = new Logger({ error: false }); + customLogger.error("This should not log"); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + test("should respect disabled success level", () => { + const customLogger = new Logger({ success: false }); + customLogger.success("This should not log"); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + test("should respect disabled info level", () => { + const customLogger = new Logger({ info: false }); + customLogger.info("This should not log"); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + test("should test all convenience methods", () => { + logger.step("Test step"); + logger.search("Test search"); + logger.ai("Test AI"); + logger.location("Test location"); + logger.file("Test file"); + expect(consoleSpy).toHaveBeenCalledTimes(5); + }); + + test("should enable all levels with verbose method", () => { + const customLogger = new Logger({ debug: false, info: false }); + customLogger.verbose(); + customLogger.debug("This should log"); + customLogger.info("This should log"); + expect(consoleSpy).toHaveBeenCalledTimes(2); + }); + + test("should handle setLevel with invalid level gracefully", () => { + const customLogger = new Logger(); + expect(() => { + customLogger.setLevel("invalid", false); + }).not.toThrow(); + }); + + test("should format messages with timestamps", () => { + logger.info("Test message"); + const loggedMessage = consoleSpy.mock.calls[0][0]; + expect(loggedMessage).toMatch(/\[\d{1,2}:\d{2}:\d{2}\]/); + }); + + test("should include level in formatted messages", () => { + logger.info("Test message"); + const loggedMessage = consoleSpy.mock.calls[0][0]; + expect(loggedMessage).toContain("[INFO]"); + }); + + test("should disable colors when colors option is false", () => { + const customLogger = new Logger({ colors: false }); + customLogger.info("Test message"); + const loggedMessage = consoleSpy.mock.calls[0][0]; + // Should not contain ANSI color codes + expect(loggedMessage).not.toMatch(/\u001b\[/); + }); + + test("should enable colors by default", () => { + logger.info("Test message"); + const loggedMessage = consoleSpy.mock.calls[0][0]; + // Should contain ANSI color codes + expect(loggedMessage).toMatch(/\u001b\[/); + }); + + test("should handle multiple level configurations", () => { + const customLogger = new Logger({ + debug: false, + info: true, + warning: false, + error: true, + success: false, + }); + + customLogger.debug("Should not log"); + customLogger.info("Should log"); + customLogger.warning("Should not log"); + customLogger.error("Should log"); + customLogger.success("Should not log"); + + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + test("should handle empty or undefined messages", () => { + expect(() => { + logger.info(""); + logger.info(undefined); + logger.info(null); + }).not.toThrow(); + }); + + test("should handle complex message objects", () => { + const testObj = { key: "value", nested: { data: "test" } }; + expect(() => { + logger.info(testObj); + }).not.toThrow(); + }); +}); diff --git a/core-parser/auth-manager.js b/core-parser/auth-manager.js index 68a5aef..3c0fcbd 100644 --- a/core-parser/auth-manager.js +++ b/core-parser/auth-manager.js @@ -1,94 +1,94 @@ -/** - * Authentication Manager - * - * Handles login/authentication for different sites - */ - -class AuthManager { - constructor(coreParser) { - this.coreParser = coreParser; - } - - /** - * Authenticate to a specific site - */ - async authenticate(site, credentials, pageId = "default") { - const strategies = { - linkedin: this.authenticateLinkedIn.bind(this), - // Add more auth strategies as needed - }; - - const strategy = strategies[site.toLowerCase()]; - if (!strategy) { - throw new Error(`No authentication strategy found for site: ${site}`); - } - - return await strategy(credentials, pageId); - } - - /** - * LinkedIn authentication strategy - */ - async authenticateLinkedIn(credentials, pageId = "default") { - const { username, password } = credentials; - if (!username || !password) { - throw new Error("LinkedIn authentication requires username and password"); - } - - const page = this.coreParser.getPage(pageId); - if (!page) { - throw new Error(`Page with ID '${pageId}' not found`); - } - - try { - // Navigate to LinkedIn login - await this.coreParser.navigateTo("https://www.linkedin.com/login", { - pageId, - }); - - // Fill credentials - await page.fill('input[name="session_key"]', username); - await page.fill('input[name="session_password"]', password); - - // Submit form - await page.click('button[type="submit"]'); - - // Wait for successful login (profile image appears) - await page.waitForSelector("img.global-nav__me-photo", { - timeout: 15000, - }); - - return true; - } catch (error) { - throw new Error(`LinkedIn authentication failed: ${error.message}`); - } - } - - /** - * Check if currently authenticated to a site - */ - async isAuthenticated(site, pageId = "default") { - const page = this.coreParser.getPage(pageId); - if (!page) { - return false; - } - - const checkers = { - linkedin: async () => { - try { - await page.waitForSelector("img.global-nav__me-photo", { - timeout: 2000, - }); - return true; - } catch { - return false; - } - }, - }; - - const checker = checkers[site.toLowerCase()]; - return checker ? await checker() : false; - } -} - -module.exports = AuthManager; +/** + * Authentication Manager + * + * Handles login/authentication for different sites + */ + +class AuthManager { + constructor(coreParser) { + this.coreParser = coreParser; + } + + /** + * Authenticate to a specific site + */ + async authenticate(site, credentials, pageId = "default") { + const strategies = { + linkedin: this.authenticateLinkedIn.bind(this), + // Add more auth strategies as needed + }; + + const strategy = strategies[site.toLowerCase()]; + if (!strategy) { + throw new Error(`No authentication strategy found for site: ${site}`); + } + + return await strategy(credentials, pageId); + } + + /** + * LinkedIn authentication strategy + */ + async authenticateLinkedIn(credentials, pageId = "default") { + const { username, password } = credentials; + if (!username || !password) { + throw new Error("LinkedIn authentication requires username and password"); + } + + const page = this.coreParser.getPage(pageId); + if (!page) { + throw new Error(`Page with ID '${pageId}' not found`); + } + + try { + // Navigate to LinkedIn login + await this.coreParser.navigateTo("https://www.linkedin.com/login", { + pageId, + }); + + // Fill credentials + await page.fill('input[name="session_key"]', username); + await page.fill('input[name="session_password"]', password); + + // Submit form + await page.click('button[type="submit"]'); + + // Wait for successful login (profile image appears) + await page.waitForSelector("img.global-nav__me-photo", { + timeout: 15000, + }); + + return true; + } catch (error) { + throw new Error(`LinkedIn authentication failed: ${error.message}`); + } + } + + /** + * Check if currently authenticated to a site + */ + async isAuthenticated(site, pageId = "default") { + const page = this.coreParser.getPage(pageId); + if (!page) { + return false; + } + + const checkers = { + linkedin: async () => { + try { + await page.waitForSelector("img.global-nav__me-photo", { + timeout: 2000, + }); + return true; + } catch { + return false; + } + }, + }; + + const checker = checkers[site.toLowerCase()]; + return checker ? await checker() : false; + } +} + +module.exports = AuthManager; diff --git a/core-parser/index.js b/core-parser/index.js new file mode 100644 index 0000000..fe1e52b --- /dev/null +++ b/core-parser/index.js @@ -0,0 +1,63 @@ +const playwright = require('playwright'); +const AuthManager = require('./auth-manager'); +const NavigationManager = require('./navigation'); + +class CoreParser { + constructor(config = {}) { + this.config = { + headless: true, + timeout: 60000, // Increased default timeout + ...config + }; + this.browser = null; + this.context = null; + this.pages = {}; + this.authManager = new AuthManager(this); + this.navigationManager = new NavigationManager(this); + } + + async init() { + this.browser = await playwright.chromium.launch({ + headless: this.config.headless + }); + this.context = await this.browser.newContext(); + } + + async createPage(id) { + if (!this.browser) await this.init(); + const page = await this.context.newPage(); + this.pages[id] = page; + return page; + } + + getPage(id) { + return this.pages[id]; + } + + async authenticate(site, credentials, pageId) { + return this.authManager.authenticate(site, credentials, pageId); + } + + async navigateTo(url, options = {}) { + const { + pageId = "default", + waitUntil = "networkidle", // Changed default to networkidle + retries = 1, + retryDelay = 2000, + timeout = this.config.timeout, + } = options; + + return this.navigationManager.navigateTo(url, options); + } + + async cleanup() { + if (this.browser) { + await this.browser.close(); + this.browser = null; + this.context = null; + this.pages = {}; + } + } +} + +module.exports = CoreParser; diff --git a/core-parser/navigation.js b/core-parser/navigation.js index 37bb713..5bc2675 100644 --- a/core-parser/navigation.js +++ b/core-parser/navigation.js @@ -1,131 +1,131 @@ -/** - * Navigation Manager - * - * Handles page navigation with error handling, retries, and logging - */ - -class NavigationManager { - constructor(coreParser) { - this.coreParser = coreParser; - } - - /** - * Navigate to URL with comprehensive error handling - */ - async navigateTo(url, options = {}) { - const { - pageId = "default", - waitUntil = "domcontentloaded", - retries = 1, - retryDelay = 2000, - timeout = this.coreParser.config.timeout, - } = options; - - const page = this.coreParser.getPage(pageId); - if (!page) { - throw new Error(`Page with ID '${pageId}' not found`); - } - - let lastError; - - for (let attempt = 0; attempt <= retries; attempt++) { - try { - console.log( - `🌐 Navigating to: ${url} (attempt ${attempt + 1}/${retries + 1})` - ); - - await page.goto(url, { - waitUntil, - timeout, - }); - - console.log(`āœ… Navigation successful: ${url}`); - return true; - } catch (error) { - lastError = error; - console.warn( - `āš ļø Navigation attempt ${attempt + 1} failed: ${error.message}` - ); - - if (attempt < retries) { - console.log(`šŸ”„ Retrying in ${retryDelay}ms...`); - await this.delay(retryDelay); - } - } - } - - // All attempts failed - const errorMessage = `Navigation failed after ${retries + 1} attempts: ${ - lastError.message - }`; - console.error(`āŒ ${errorMessage}`); - throw new Error(errorMessage); - } - - /** - * Navigate and wait for specific selector - */ - async navigateAndWaitFor(url, selector, options = {}) { - await this.navigateTo(url, options); - - const { pageId = "default", timeout = this.coreParser.config.timeout } = - options; - const page = this.coreParser.getPage(pageId); - - try { - await page.waitForSelector(selector, { timeout }); - console.log(`āœ… Selector found: ${selector}`); - return true; - } catch (error) { - console.warn(`āš ļø Selector not found: ${selector} - ${error.message}`); - return false; - } - } - - /** - * Check if current page has specific content - */ - async hasContent(content, options = {}) { - const { pageId = "default", timeout = 5000 } = options; - const page = this.coreParser.getPage(pageId); - - try { - await page.waitForFunction( - (text) => document.body.innerText.includes(text), - content, - { timeout } - ); - return true; - } catch { - return false; - } - } - - /** - * Utility delay function - */ - async delay(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - /** - * Get current page URL - */ - getCurrentUrl(pageId = "default") { - const page = this.coreParser.getPage(pageId); - return page ? page.url() : null; - } - - /** - * Take screenshot for debugging - */ - async screenshot(filepath, pageId = "default") { - const page = this.coreParser.getPage(pageId); - if (page) { - await page.screenshot({ path: filepath }); - console.log(`šŸ“ø Screenshot saved: ${filepath}`); - } - } -} - -module.exports = NavigationManager; +/** + * Navigation Manager + * + * Handles page navigation with error handling, retries, and logging + */ + +class NavigationManager { + constructor(coreParser) { + this.coreParser = coreParser; + } + + /** + * Navigate to URL with comprehensive error handling + */ + async navigateTo(url, options = {}) { + const { + pageId = "default", + waitUntil = "domcontentloaded", + retries = 1, + retryDelay = 2000, + timeout = this.coreParser.config.timeout, + } = options; + + const page = this.coreParser.getPage(pageId); + if (!page) { + throw new Error(`Page with ID '${pageId}' not found`); + } + + let lastError; + + for (let attempt = 0; attempt <= retries; attempt++) { + try { + console.log( + `🌐 Navigating to: ${url} (attempt ${attempt + 1}/${retries + 1})` + ); + + await page.goto(url, { + waitUntil, + timeout, + }); + + console.log(`āœ… Navigation successful: ${url}`); + return true; + } catch (error) { + lastError = error; + console.warn( + `āš ļø Navigation attempt ${attempt + 1} failed: ${error.message}` + ); + + if (attempt < retries) { + console.log(`šŸ”„ Retrying in ${retryDelay}ms...`); + await this.delay(retryDelay); + } + } + } + + // All attempts failed + const errorMessage = `Navigation failed after ${retries + 1} attempts: ${ + lastError.message + }`; + console.error(`āŒ ${errorMessage}`); + throw new Error(errorMessage); + } + + /** + * Navigate and wait for specific selector + */ + async navigateAndWaitFor(url, selector, options = {}) { + await this.navigateTo(url, options); + + const { pageId = "default", timeout = this.coreParser.config.timeout } = + options; + const page = this.coreParser.getPage(pageId); + + try { + await page.waitForSelector(selector, { timeout }); + console.log(`āœ… Selector found: ${selector}`); + return true; + } catch (error) { + console.warn(`āš ļø Selector not found: ${selector} - ${error.message}`); + return false; + } + } + + /** + * Check if current page has specific content + */ + async hasContent(content, options = {}) { + const { pageId = "default", timeout = 5000 } = options; + const page = this.coreParser.getPage(pageId); + + try { + await page.waitForFunction( + (text) => document.body.innerText.includes(text), + content, + { timeout } + ); + return true; + } catch { + return false; + } + } + + /** + * Utility delay function + */ + async delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + /** + * Get current page URL + */ + getCurrentUrl(pageId = "default") { + const page = this.coreParser.getPage(pageId); + return page ? page.url() : null; + } + + /** + * Take screenshot for debugging + */ + async screenshot(filepath, pageId = "default") { + const page = this.coreParser.getPage(pageId); + if (page) { + await page.screenshot({ path: filepath }); + console.log(`šŸ“ø Screenshot saved: ${filepath}`); + } + } +} + +module.exports = NavigationManager; diff --git a/core-parser/package.json b/core-parser/package.json index a98dafc..78a7556 100644 --- a/core-parser/package.json +++ b/core-parser/package.json @@ -1,27 +1,7 @@ -{ - "name": "core-parser", - "version": "1.0.0", - "description": "Core browser automation and parsing engine for all parsers", - "main": "index.js", - "scripts": { - "test": "jest", - "install:browsers": "npx playwright install chromium" - }, - "keywords": [ - "parser", - "playwright", - "browser", - "automation", - "core" - ], - "author": "Job Market Intelligence Team", - "license": "ISC", - "type": "commonjs", - "dependencies": { - "playwright": "^1.53.2", - "dotenv": "^17.0.0" - }, - "devDependencies": { - "jest": "^29.7.0" - } -} +{ + "name": "core-parser", + "version": "1.0.0", + "main": "index.js", + "description": "Core parser utilities for browser management", + "dependencies": {} +} diff --git a/job-search-parser/README.md b/job-search-parser/README.md index f66d73c..487b08e 100644 --- a/job-search-parser/README.md +++ b/job-search-parser/README.md @@ -1,497 +1,497 @@ -# Job Search Parser - Job Market Intelligence - -Specialized parser for job market intelligence, tracking job postings, market trends, and competitive analysis. Focuses on tech roles and industry insights. - -## šŸŽÆ Purpose - -The Job Search Parser is designed to: - -- **Track Job Market Trends**: Monitor demand for specific roles and skills -- **Competitive Intelligence**: Analyze salary ranges and requirements -- **Industry Insights**: Track hiring patterns across different sectors -- **Skill Gap Analysis**: Identify in-demand technologies and frameworks -- **Market Demand Forecasting**: Predict job market trends - -## šŸš€ Features - -### Core Functionality - -- **Multi-Source Aggregation**: Collect job data from multiple platforms -- **Role-Specific Tracking**: Focus on tech roles and emerging positions -- **Skill Analysis**: Extract and categorize required skills -- **Salary Intelligence**: Track compensation ranges and trends -- **Company Intelligence**: Monitor hiring companies and patterns - -### Advanced Features - -- **Market Trend Analysis**: Identify growing and declining job categories -- **Geographic Distribution**: Track job distribution by location -- **Experience Level Analysis**: Entry, mid, senior level tracking -- **Remote Work Trends**: Monitor remote/hybrid work patterns -- **Technology Stack Tracking**: Framework and tool popularity - -## 🌐 Supported Job Sites - -### āœ… Implemented Parsers - -#### SkipTheDrive Parser - -Remote job board specializing in work-from-home positions. - -**Features:** - -- Keyword-based job search with relevance sorting -- Job type filtering (full-time, part-time, contract) -- Multi-page result parsing with pagination -- Featured/sponsored job identification -- AI-powered job relevance analysis -- Automatic duplicate detection - -**Usage:** - -```bash -# Parse SkipTheDrive for QA automation jobs -node index.js --sites=skipthedrive --keywords="automation qa,qa engineer" - -# Filter by job type -JOB_TYPES="full time,contract" node index.js --sites=skipthedrive - -# Run demo with limited results -node index.js --sites=skipthedrive --demo -``` - -### 🚧 Planned Parsers - -- **Indeed**: Comprehensive job aggregator -- **Glassdoor**: Jobs with company reviews and salary data -- **Monster**: Traditional job board -- **SimplyHired**: Job aggregator with salary estimates -- **LinkedIn Jobs**: Professional network job postings -- **AngelList**: Startup and tech jobs -- **Remote.co**: Dedicated remote work jobs -- **FlexJobs**: Flexible and remote positions - -## šŸ“¦ Installation - -```bash -# Install dependencies -npm install - -# Run tests -npm test - -# Run demo -node demo.js -``` - -## šŸ”§ Configuration - -### Environment Variables - -Create a `.env` file in the parser directory: - -```env -# Job Search Configuration -SEARCH_SOURCES=linkedin,indeed,glassdoor -TARGET_ROLES=software engineer,data scientist,product manager -LOCATION_FILTER=Toronto,Vancouver,Calgary -EXPERIENCE_LEVELS=entry,mid,senior -REMOTE_PREFERENCE=remote,hybrid,onsite - -# Analysis Configuration -ENABLE_SALARY_ANALYSIS=true -ENABLE_SKILL_ANALYSIS=true -ENABLE_TREND_ANALYSIS=true -MIN_SALARY=50000 -MAX_SALARY=200000 - -# Output Configuration -OUTPUT_FORMAT=json,csv -SAVE_RAW_DATA=true -ANALYSIS_INTERVAL=daily -``` - -### Command Line Options - -```bash -# Basic usage -node index.js - -# Specific roles -node index.js --roles="frontend developer,backend developer" - -# Geographic focus -node index.js --locations="Toronto,Vancouver" - -# Experience level -node index.js --experience="senior" - -# Output format -node index.js --output=results/job-market-analysis.json -``` - -**Available Options:** - -- `--roles="role1,role2"`: Target job roles -- `--locations="city1,city2"`: Geographic focus -- `--experience="entry|mid|senior"`: Experience level -- `--remote="remote|hybrid|onsite"`: Remote work preference -- `--salary-min=NUMBER`: Minimum salary filter -- `--salary-max=NUMBER`: Maximum salary filter -- `--output=FILE`: Output filename -- `--format=json|csv`: Output format -- `--trends`: Enable trend analysis -- `--skills`: Enable skill analysis - -## šŸ“Š Keywords - -### Role-Specific Keywords - -Place keyword CSV files in the `keywords/` directory: - -``` -job-search-parser/ -ā”œā”€ā”€ keywords/ -│ ā”œā”€ā”€ job-search-keywords.csv # General job search terms -│ ā”œā”€ā”€ tech-roles.csv # Technology roles -│ ā”œā”€ā”€ data-roles.csv # Data science roles -│ ā”œā”€ā”€ management-roles.csv # Management positions -│ └── emerging-roles.csv # Emerging job categories -└── index.js -``` - -### Tech Roles Keywords - -```csv -keyword -software engineer -frontend developer -backend developer -full stack developer -data scientist -machine learning engineer -devops engineer -site reliability engineer -cloud architect -security engineer -mobile developer -iOS developer -Android developer -react developer -vue developer -angular developer -node.js developer -python developer -java developer -golang developer -rust developer -data engineer -analytics engineer -``` - -### Data Science Keywords - -```csv -keyword -data scientist -machine learning engineer -data analyst -business analyst -data engineer -analytics engineer -ML engineer -AI engineer -statistician -quantitative analyst -research scientist -data architect -BI developer -ETL developer -``` - -## šŸ“ˆ Usage Examples - -### Basic Job Search - -```bash -# Standard job market analysis -node index.js - -# Specific tech roles -node index.js --roles="software engineer,data scientist" - -# Geographic focus -node index.js --locations="Toronto,Vancouver,Calgary" -``` - -### Advanced Analysis - -```bash -# Senior level positions -node index.js --experience="senior" --salary-min=100000 - -# Remote work opportunities -node index.js --remote="remote" --roles="frontend developer" - -# Trend analysis -node index.js --trends --skills --output=results/trends.json -``` - -### Market Intelligence - -```bash -# Salary analysis -node index.js --salary-min=80000 --salary-max=150000 - -# Skill gap analysis -node index.js --skills --roles="machine learning engineer" - -# Competitive intelligence -node index.js --companies="Google,Microsoft,Amazon" -``` - -## šŸ“Š Output Format - -### JSON Structure - -```json -{ - "metadata": { - "timestamp": "2024-01-15T10:30:00Z", - "search_parameters": { - "roles": ["software engineer", "data scientist"], - "locations": ["Toronto", "Vancouver"], - "experience_levels": ["mid", "senior"], - "remote_preference": ["remote", "hybrid"] - }, - "total_jobs_found": 1250, - "analysis_duration_seconds": 45 - }, - "market_overview": { - "total_jobs": 1250, - "average_salary": 95000, - "salary_range": { - "min": 65000, - "max": 180000, - "median": 92000 - }, - "remote_distribution": { - "remote": 45, - "hybrid": 35, - "onsite": 20 - }, - "experience_distribution": { - "entry": 15, - "mid": 45, - "senior": 40 - } - }, - "trends": { - "growing_skills": [ - { "skill": "React", "growth_rate": 25 }, - { "skill": "Python", "growth_rate": 18 }, - { "skill": "AWS", "growth_rate": 22 } - ], - "declining_skills": [ - { "skill": "jQuery", "growth_rate": -12 }, - { "skill": "PHP", "growth_rate": -8 } - ], - "emerging_roles": ["AI Engineer", "DevSecOps Engineer", "Data Engineer"] - }, - "jobs": [ - { - "id": "job_1", - "title": "Senior Software Engineer", - "company": "TechCorp", - "location": "Toronto, Ontario", - "remote_type": "hybrid", - "salary": { - "min": 100000, - "max": 140000, - "currency": "CAD" - }, - "required_skills": ["React", "Node.js", "TypeScript", "AWS"], - "preferred_skills": ["GraphQL", "Docker", "Kubernetes"], - "experience_level": "senior", - "job_url": "https://example.com/job/1", - "posted_date": "2024-01-10T09:00:00Z", - "scraped_at": "2024-01-15T10:30:00Z" - } - ], - "analysis": { - "skill_demand": { - "React": { "count": 45, "avg_salary": 98000 }, - "Python": { "count": 38, "avg_salary": 102000 }, - "AWS": { "count": 32, "avg_salary": 105000 } - }, - "company_insights": { - "top_hirers": [ - { "company": "TechCorp", "jobs": 25 }, - { "company": "StartupXYZ", "jobs": 18 } - ], - "salary_leaders": [ - { "company": "BigTech", "avg_salary": 120000 }, - { "company": "FinTech", "avg_salary": 115000 } - ] - } - } -} -``` - -### CSV Output - -The parser can also generate CSV files for easy analysis: - -```csv -job_id,title,company,location,remote_type,salary_min,salary_max,required_skills,experience_level,posted_date -job_1,Senior Software Engineer,TechCorp,Toronto,hybrid,100000,140000,"React,Node.js,TypeScript",senior,2024-01-10 -job_2,Data Scientist,DataCorp,Vancouver,remote,90000,130000,"Python,SQL,ML",mid,2024-01-09 -``` - -## šŸ”’ Security & Best Practices - -### Data Privacy - -- Respect job site terms of service -- Implement appropriate rate limiting -- Store data securely and responsibly -- Anonymize sensitive information - -### Rate Limiting - -- Implement delays between requests -- Respect API rate limits -- Use multiple data sources -- Monitor for blocking/detection - -### Legal Compliance - -- Educational and research purposes only -- Respect website terms of service -- Implement data retention policies -- Monitor for legal changes - -## 🧪 Testing - -### Run Tests - -```bash -# All tests -npm test - -# Specific test suites -npm test -- --testNamePattern="JobSearch" -npm test -- --testNamePattern="Analysis" -npm test -- --testNamePattern="Trends" -``` - -### Test Coverage - -```bash -npm run test:coverage -``` - -## šŸš€ Performance Optimization - -### Recommended Settings - -#### Fast Analysis - -```bash -node index.js --roles="software engineer" --locations="Toronto" -``` - -#### Comprehensive Analysis - -```bash -node index.js --trends --skills --experience="all" -``` - -#### Focused Intelligence - -```bash -node index.js --salary-min=80000 --remote="remote" --trends -``` - -### Performance Tips - -- Use specific role filters to reduce data volume -- Implement caching for repeated searches -- Use parallel processing for multiple sources -- Optimize data storage and retrieval - -## šŸ”§ Troubleshooting - -### Common Issues - -#### Rate Limiting - -```bash -# Reduce request frequency -export REQUEST_DELAY=2000 -node index.js -``` - -#### Data Source Issues - -```bash -# Use specific sources -node index.js --sources="linkedin,indeed" - -# Check source availability -node index.js --test-sources -``` - -#### Output Issues - -```bash -# Check output directory -mkdir -p results -node index.js --output=results/analysis.json - -# Verify file permissions -chmod 755 results/ -``` - -## šŸ“ˆ Monitoring & Analytics - -### Key Metrics - -- **Job Volume**: Total jobs found per search -- **Salary Trends**: Average and median salary changes -- **Skill Demand**: Most requested skills -- **Remote Adoption**: Remote work trend analysis -- **Market Velocity**: Job posting frequency - -### Dashboard Integration - -- Real-time market monitoring -- Trend visualization -- Salary benchmarking -- Skill gap analysis -- Competitive intelligence - -## šŸ¤ Contributing - -### Development Setup - -1. Fork the repository -2. Create feature branch -3. Add tests for new functionality -4. Ensure all tests pass -5. Submit pull request - -### Code Standards - -- Follow existing code style -- Add JSDoc comments -- Maintain test coverage -- Update documentation - -## šŸ“„ License - -This parser is part of the LinkedOut platform and follows the same licensing terms. - ---- - -**Note**: This tool is designed for educational and research purposes. Always respect website terms of service and implement appropriate rate limiting and ethical usage practices. +# Job Search Parser - Job Market Intelligence + +Specialized parser for job market intelligence, tracking job postings, market trends, and competitive analysis. Focuses on tech roles and industry insights. + +## šŸŽÆ Purpose + +The Job Search Parser is designed to: + +- **Track Job Market Trends**: Monitor demand for specific roles and skills +- **Competitive Intelligence**: Analyze salary ranges and requirements +- **Industry Insights**: Track hiring patterns across different sectors +- **Skill Gap Analysis**: Identify in-demand technologies and frameworks +- **Market Demand Forecasting**: Predict job market trends + +## šŸš€ Features + +### Core Functionality + +- **Multi-Source Aggregation**: Collect job data from multiple platforms +- **Role-Specific Tracking**: Focus on tech roles and emerging positions +- **Skill Analysis**: Extract and categorize required skills +- **Salary Intelligence**: Track compensation ranges and trends +- **Company Intelligence**: Monitor hiring companies and patterns + +### Advanced Features + +- **Market Trend Analysis**: Identify growing and declining job categories +- **Geographic Distribution**: Track job distribution by location +- **Experience Level Analysis**: Entry, mid, senior level tracking +- **Remote Work Trends**: Monitor remote/hybrid work patterns +- **Technology Stack Tracking**: Framework and tool popularity + +## 🌐 Supported Job Sites + +### āœ… Implemented Parsers + +#### SkipTheDrive Parser + +Remote job board specializing in work-from-home positions. + +**Features:** + +- Keyword-based job search with relevance sorting +- Job type filtering (full-time, part-time, contract) +- Multi-page result parsing with pagination +- Featured/sponsored job identification +- AI-powered job relevance analysis +- Automatic duplicate detection + +**Usage:** + +```bash +# Parse SkipTheDrive for QA automation jobs +node index.js --sites=skipthedrive --keywords="automation qa,qa engineer" + +# Filter by job type +JOB_TYPES="full time,contract" node index.js --sites=skipthedrive + +# Run demo with limited results +node index.js --sites=skipthedrive --demo +``` + +### 🚧 Planned Parsers + +- **Indeed**: Comprehensive job aggregator +- **Glassdoor**: Jobs with company reviews and salary data +- **Monster**: Traditional job board +- **SimplyHired**: Job aggregator with salary estimates +- **LinkedIn Jobs**: Professional network job postings +- **AngelList**: Startup and tech jobs +- **Remote.co**: Dedicated remote work jobs +- **FlexJobs**: Flexible and remote positions + +## šŸ“¦ Installation + +```bash +# Install dependencies +npm install + +# Run tests +npm test + +# Run demo +node demo.js +``` + +## šŸ”§ Configuration + +### Environment Variables + +Create a `.env` file in the parser directory: + +```env +# Job Search Configuration +SEARCH_SOURCES=linkedin,indeed,glassdoor +TARGET_ROLES=software engineer,data scientist,product manager +LOCATION_FILTER=Toronto,Vancouver,Calgary +EXPERIENCE_LEVELS=entry,mid,senior +REMOTE_PREFERENCE=remote,hybrid,onsite + +# Analysis Configuration +ENABLE_SALARY_ANALYSIS=true +ENABLE_SKILL_ANALYSIS=true +ENABLE_TREND_ANALYSIS=true +MIN_SALARY=50000 +MAX_SALARY=200000 + +# Output Configuration +OUTPUT_FORMAT=json,csv +SAVE_RAW_DATA=true +ANALYSIS_INTERVAL=daily +``` + +### Command Line Options + +```bash +# Basic usage +node index.js + +# Specific roles +node index.js --roles="frontend developer,backend developer" + +# Geographic focus +node index.js --locations="Toronto,Vancouver" + +# Experience level +node index.js --experience="senior" + +# Output format +node index.js --output=results/job-market-analysis.json +``` + +**Available Options:** + +- `--roles="role1,role2"`: Target job roles +- `--locations="city1,city2"`: Geographic focus +- `--experience="entry|mid|senior"`: Experience level +- `--remote="remote|hybrid|onsite"`: Remote work preference +- `--salary-min=NUMBER`: Minimum salary filter +- `--salary-max=NUMBER`: Maximum salary filter +- `--output=FILE`: Output filename +- `--format=json|csv`: Output format +- `--trends`: Enable trend analysis +- `--skills`: Enable skill analysis + +## šŸ“Š Keywords + +### Role-Specific Keywords + +Place keyword CSV files in the `keywords/` directory: + +``` +job-search-parser/ +ā”œā”€ā”€ keywords/ +│ ā”œā”€ā”€ job-search-keywords.csv # General job search terms +│ ā”œā”€ā”€ tech-roles.csv # Technology roles +│ ā”œā”€ā”€ data-roles.csv # Data science roles +│ ā”œā”€ā”€ management-roles.csv # Management positions +│ └── emerging-roles.csv # Emerging job categories +└── index.js +``` + +### Tech Roles Keywords + +```csv +keyword +software engineer +frontend developer +backend developer +full stack developer +data scientist +machine learning engineer +devops engineer +site reliability engineer +cloud architect +security engineer +mobile developer +iOS developer +Android developer +react developer +vue developer +angular developer +node.js developer +python developer +java developer +golang developer +rust developer +data engineer +analytics engineer +``` + +### Data Science Keywords + +```csv +keyword +data scientist +machine learning engineer +data analyst +business analyst +data engineer +analytics engineer +ML engineer +AI engineer +statistician +quantitative analyst +research scientist +data architect +BI developer +ETL developer +``` + +## šŸ“ˆ Usage Examples + +### Basic Job Search + +```bash +# Standard job market analysis +node index.js + +# Specific tech roles +node index.js --roles="software engineer,data scientist" + +# Geographic focus +node index.js --locations="Toronto,Vancouver,Calgary" +``` + +### Advanced Analysis + +```bash +# Senior level positions +node index.js --experience="senior" --salary-min=100000 + +# Remote work opportunities +node index.js --remote="remote" --roles="frontend developer" + +# Trend analysis +node index.js --trends --skills --output=results/trends.json +``` + +### Market Intelligence + +```bash +# Salary analysis +node index.js --salary-min=80000 --salary-max=150000 + +# Skill gap analysis +node index.js --skills --roles="machine learning engineer" + +# Competitive intelligence +node index.js --companies="Google,Microsoft,Amazon" +``` + +## šŸ“Š Output Format + +### JSON Structure + +```json +{ + "metadata": { + "timestamp": "2024-01-15T10:30:00Z", + "search_parameters": { + "roles": ["software engineer", "data scientist"], + "locations": ["Toronto", "Vancouver"], + "experience_levels": ["mid", "senior"], + "remote_preference": ["remote", "hybrid"] + }, + "total_jobs_found": 1250, + "analysis_duration_seconds": 45 + }, + "market_overview": { + "total_jobs": 1250, + "average_salary": 95000, + "salary_range": { + "min": 65000, + "max": 180000, + "median": 92000 + }, + "remote_distribution": { + "remote": 45, + "hybrid": 35, + "onsite": 20 + }, + "experience_distribution": { + "entry": 15, + "mid": 45, + "senior": 40 + } + }, + "trends": { + "growing_skills": [ + { "skill": "React", "growth_rate": 25 }, + { "skill": "Python", "growth_rate": 18 }, + { "skill": "AWS", "growth_rate": 22 } + ], + "declining_skills": [ + { "skill": "jQuery", "growth_rate": -12 }, + { "skill": "PHP", "growth_rate": -8 } + ], + "emerging_roles": ["AI Engineer", "DevSecOps Engineer", "Data Engineer"] + }, + "jobs": [ + { + "id": "job_1", + "title": "Senior Software Engineer", + "company": "TechCorp", + "location": "Toronto, Ontario", + "remote_type": "hybrid", + "salary": { + "min": 100000, + "max": 140000, + "currency": "CAD" + }, + "required_skills": ["React", "Node.js", "TypeScript", "AWS"], + "preferred_skills": ["GraphQL", "Docker", "Kubernetes"], + "experience_level": "senior", + "job_url": "https://example.com/job/1", + "posted_date": "2024-01-10T09:00:00Z", + "scraped_at": "2024-01-15T10:30:00Z" + } + ], + "analysis": { + "skill_demand": { + "React": { "count": 45, "avg_salary": 98000 }, + "Python": { "count": 38, "avg_salary": 102000 }, + "AWS": { "count": 32, "avg_salary": 105000 } + }, + "company_insights": { + "top_hirers": [ + { "company": "TechCorp", "jobs": 25 }, + { "company": "StartupXYZ", "jobs": 18 } + ], + "salary_leaders": [ + { "company": "BigTech", "avg_salary": 120000 }, + { "company": "FinTech", "avg_salary": 115000 } + ] + } + } +} +``` + +### CSV Output + +The parser can also generate CSV files for easy analysis: + +```csv +job_id,title,company,location,remote_type,salary_min,salary_max,required_skills,experience_level,posted_date +job_1,Senior Software Engineer,TechCorp,Toronto,hybrid,100000,140000,"React,Node.js,TypeScript",senior,2024-01-10 +job_2,Data Scientist,DataCorp,Vancouver,remote,90000,130000,"Python,SQL,ML",mid,2024-01-09 +``` + +## šŸ”’ Security & Best Practices + +### Data Privacy + +- Respect job site terms of service +- Implement appropriate rate limiting +- Store data securely and responsibly +- Anonymize sensitive information + +### Rate Limiting + +- Implement delays between requests +- Respect API rate limits +- Use multiple data sources +- Monitor for blocking/detection + +### Legal Compliance + +- Educational and research purposes only +- Respect website terms of service +- Implement data retention policies +- Monitor for legal changes + +## 🧪 Testing + +### Run Tests + +```bash +# All tests +npm test + +# Specific test suites +npm test -- --testNamePattern="JobSearch" +npm test -- --testNamePattern="Analysis" +npm test -- --testNamePattern="Trends" +``` + +### Test Coverage + +```bash +npm run test:coverage +``` + +## šŸš€ Performance Optimization + +### Recommended Settings + +#### Fast Analysis + +```bash +node index.js --roles="software engineer" --locations="Toronto" +``` + +#### Comprehensive Analysis + +```bash +node index.js --trends --skills --experience="all" +``` + +#### Focused Intelligence + +```bash +node index.js --salary-min=80000 --remote="remote" --trends +``` + +### Performance Tips + +- Use specific role filters to reduce data volume +- Implement caching for repeated searches +- Use parallel processing for multiple sources +- Optimize data storage and retrieval + +## šŸ”§ Troubleshooting + +### Common Issues + +#### Rate Limiting + +```bash +# Reduce request frequency +export REQUEST_DELAY=2000 +node index.js +``` + +#### Data Source Issues + +```bash +# Use specific sources +node index.js --sources="linkedin,indeed" + +# Check source availability +node index.js --test-sources +``` + +#### Output Issues + +```bash +# Check output directory +mkdir -p results +node index.js --output=results/analysis.json + +# Verify file permissions +chmod 755 results/ +``` + +## šŸ“ˆ Monitoring & Analytics + +### Key Metrics + +- **Job Volume**: Total jobs found per search +- **Salary Trends**: Average and median salary changes +- **Skill Demand**: Most requested skills +- **Remote Adoption**: Remote work trend analysis +- **Market Velocity**: Job posting frequency + +### Dashboard Integration + +- Real-time market monitoring +- Trend visualization +- Salary benchmarking +- Skill gap analysis +- Competitive intelligence + +## šŸ¤ Contributing + +### Development Setup + +1. Fork the repository +2. Create feature branch +3. Add tests for new functionality +4. Ensure all tests pass +5. Submit pull request + +### Code Standards + +- Follow existing code style +- Add JSDoc comments +- Maintain test coverage +- Update documentation + +## šŸ“„ License + +This parser is part of the LinkedOut platform and follows the same licensing terms. + +--- + +**Note**: This tool is designed for educational and research purposes. Always respect website terms of service and implement appropriate rate limiting and ethical usage practices. diff --git a/job-search-parser/demo.js b/job-search-parser/demo.js index c3e1987..c768bcb 100644 --- a/job-search-parser/demo.js +++ b/job-search-parser/demo.js @@ -1,543 +1,543 @@ -/** - * Job Search Parser Demo - * - * Demonstrates the Job Search Parser's capabilities for job market intelligence, - * trend analysis, and competitive insights. - * - * This demo uses simulated data for demonstration purposes. - */ - -const { logger } = require("../ai-analyzer"); -const fs = require("fs"); -const path = require("path"); - -// Terminal colors for demo output -const colors = { - reset: "\x1b[0m", - bright: "\x1b[1m", - cyan: "\x1b[36m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - magenta: "\x1b[35m", - red: "\x1b[31m", -}; - -const demo = { - title: (text) => - console.log(`\n${colors.bright}${colors.cyan}${text}${colors.reset}`), - section: (text) => - console.log(`\n${colors.bright}${colors.magenta}${text}${colors.reset}`), - success: (text) => console.log(`${colors.green}āœ… ${text}${colors.reset}`), - info: (text) => console.log(`${colors.blue}ā„¹ļø ${text}${colors.reset}`), - warning: (text) => console.log(`${colors.yellow}āš ļø ${text}${colors.reset}`), - error: (text) => console.log(`${colors.red}āŒ ${text}${colors.reset}`), - code: (text) => console.log(`${colors.cyan}${text}${colors.reset}`), -}; - -// Mock job data for demonstration -const mockJobs = [ - { - id: "job_1", - title: "Senior Software Engineer", - company: "TechCorp", - location: "Toronto, Ontario", - remote_type: "hybrid", - salary: { min: 100000, max: 140000, currency: "CAD" }, - required_skills: ["React", "Node.js", "TypeScript", "AWS"], - preferred_skills: ["GraphQL", "Docker", "Kubernetes"], - experience_level: "senior", - job_url: "https://example.com/job/1", - posted_date: "2024-01-10T09:00:00Z", - scraped_at: "2024-01-15T10:30:00Z", - }, - { - id: "job_2", - title: "Data Scientist", - company: "DataCorp", - location: "Vancouver, British Columbia", - remote_type: "remote", - salary: { min: 90000, max: 130000, currency: "CAD" }, - required_skills: ["Python", "SQL", "Machine Learning", "Statistics"], - preferred_skills: ["TensorFlow", "PyTorch", "AWS"], - experience_level: "mid", - job_url: "https://example.com/job/2", - posted_date: "2024-01-09T14:30:00Z", - scraped_at: "2024-01-15T10:30:00Z", - }, - { - id: "job_3", - title: "Frontend Developer", - company: "StartupXYZ", - location: "Calgary, Alberta", - remote_type: "onsite", - salary: { min: 70000, max: 95000, currency: "CAD" }, - required_skills: ["React", "JavaScript", "CSS", "HTML"], - preferred_skills: ["Vue.js", "TypeScript", "Webpack"], - experience_level: "entry", - job_url: "https://example.com/job/3", - posted_date: "2024-01-08T11:15:00Z", - scraped_at: "2024-01-15T10:30:00Z", - }, - { - id: "job_4", - title: "DevOps Engineer", - company: "CloudTech", - location: "Toronto, Ontario", - remote_type: "hybrid", - salary: { min: 95000, max: 125000, currency: "CAD" }, - required_skills: ["Docker", "Kubernetes", "AWS", "Linux"], - preferred_skills: ["Terraform", "Jenkins", "Prometheus"], - experience_level: "senior", - job_url: "https://example.com/job/4", - posted_date: "2024-01-07T16:45:00Z", - scraped_at: "2024-01-15T10:30:00Z", - }, - { - id: "job_5", - title: "Machine Learning Engineer", - company: "AI Solutions", - location: "Vancouver, British Columbia", - remote_type: "remote", - salary: { min: 110000, max: 150000, currency: "CAD" }, - required_skills: ["Python", "TensorFlow", "PyTorch", "ML"], - preferred_skills: ["AWS", "Docker", "Kubernetes", "Spark"], - experience_level: "senior", - job_url: "https://example.com/job/5", - posted_date: "2024-01-06T10:20:00Z", - scraped_at: "2024-01-15T10:30:00Z", - }, -]; - -async function runDemo() { - demo.title("=== Job Search Parser Demo ==="); - demo.info( - "This demo showcases the Job Search Parser's capabilities for job market intelligence." - ); - demo.info("All data shown is simulated for demonstration purposes."); - demo.info("Press Enter to continue through each section...\n"); - - await waitForEnter(); - - // 1. Configuration Demo - await demonstrateConfiguration(); - - // 2. Job Search Process Demo - await demonstrateJobSearch(); - - // 3. Market Analysis Demo - await demonstrateMarketAnalysis(); - - // 4. Trend Analysis Demo - await demonstrateTrendAnalysis(); - - // 5. Skill Analysis Demo - await demonstrateSkillAnalysis(); - - // 6. Competitive Intelligence Demo - await demonstrateCompetitiveIntelligence(); - - // 7. Output Generation Demo - await demonstrateOutputGeneration(); - - demo.title("=== Demo Complete ==="); - demo.success("Job Search Parser demo completed successfully!"); - demo.info("Check the README.md for detailed usage instructions."); -} - -async function demonstrateConfiguration() { - demo.section("1. Configuration Setup"); - demo.info( - "The Job Search Parser uses environment variables and command-line options for configuration." - ); - - demo.code("// Environment Variables (.env file)"); - demo.info("SEARCH_SOURCES=linkedin,indeed,glassdoor"); - demo.info("TARGET_ROLES=software engineer,data scientist,product manager"); - demo.info("LOCATION_FILTER=Toronto,Vancouver,Calgary"); - demo.info("EXPERIENCE_LEVELS=entry,mid,senior"); - demo.info("REMOTE_PREFERENCE=remote,hybrid,onsite"); - demo.info("ENABLE_SALARY_ANALYSIS=true"); - demo.info("ENABLE_SKILL_ANALYSIS=true"); - demo.info("ENABLE_TREND_ANALYSIS=true"); - - demo.code("// Command Line Options"); - demo.info('node index.js --roles="frontend developer,backend developer"'); - demo.info('node index.js --locations="Toronto,Vancouver"'); - demo.info('node index.js --experience="senior" --salary-min=100000'); - demo.info('node index.js --remote="remote" --trends --skills'); - - await waitForEnter(); -} - -async function demonstrateJobSearch() { - demo.section("2. Job Search Process"); - demo.info( - "The parser searches multiple job platforms for relevant positions." - ); - - const searchSources = ["LinkedIn", "Indeed", "Glassdoor"]; - const targetRoles = [ - "Software Engineer", - "Data Scientist", - "Frontend Developer", - ]; - - demo.code("// Multi-source job search"); - for (const source of searchSources) { - logger.search(`Searching ${source} for job postings...`); - await simulateSearch(); - - const jobsFound = Math.floor(Math.random() * 200) + 50; - logger.success(`Found ${jobsFound} jobs on ${source}`); - } - - demo.code("// Role-specific filtering"); - for (const role of targetRoles) { - logger.info(`Filtering for ${role} positions...`); - await simulateProcessing(); - - const roleJobs = Math.floor(Math.random() * 30) + 10; - logger.success(`Found ${roleJobs} ${role} positions`); - } - - await waitForEnter(); -} - -async function demonstrateMarketAnalysis() { - demo.section("3. Market Analysis"); - demo.info( - "The parser analyzes market trends, salary ranges, and job distribution." - ); - - demo.code("// Market overview analysis"); - logger.info("Analyzing market overview..."); - await simulateProcessing(); - - const marketOverview = { - total_jobs: 1250, - average_salary: 95000, - salary_range: { min: 65000, max: 180000, median: 92000 }, - remote_distribution: { remote: 45, hybrid: 35, onsite: 20 }, - experience_distribution: { entry: 15, mid: 45, senior: 40 }, - }; - - demo.success(`Total jobs found: ${marketOverview.total_jobs}`); - demo.info( - `Average salary: $${marketOverview.average_salary.toLocaleString()}` - ); - demo.info( - `Salary range: $${marketOverview.salary_range.min.toLocaleString()} - $${marketOverview.salary_range.max.toLocaleString()}` - ); - demo.info( - `Remote work: ${marketOverview.remote_distribution.remote}% remote, ${marketOverview.remote_distribution.hybrid}% hybrid` - ); - - demo.code("// Geographic distribution"); - const locations = { - Toronto: 45, - Vancouver: 30, - Calgary: 15, - Other: 10, - }; - - Object.entries(locations).forEach(([location, percentage]) => { - demo.info(`${location}: ${percentage}% of jobs`); - }); - - await waitForEnter(); -} - -async function demonstrateTrendAnalysis() { - demo.section("4. Trend Analysis"); - demo.info( - "The parser identifies growing and declining skills and emerging roles." - ); - - demo.code("// Skill trend analysis"); - logger.info("Analyzing skill trends..."); - await simulateProcessing(); - - const growingSkills = [ - { skill: "React", growth_rate: 25 }, - { skill: "Python", growth_rate: 18 }, - { skill: "AWS", growth_rate: 22 }, - { skill: "TypeScript", growth_rate: 30 }, - { skill: "Docker", growth_rate: 15 }, - ]; - - const decliningSkills = [ - { skill: "jQuery", growth_rate: -12 }, - { skill: "PHP", growth_rate: -8 }, - { skill: "Angular", growth_rate: -5 }, - ]; - - demo.success("Growing skills:"); - growingSkills.forEach((skill) => { - demo.info(` ${skill.skill}: +${skill.growth_rate}% growth`); - }); - - demo.warning("Declining skills:"); - decliningSkills.forEach((skill) => { - demo.info(` ${skill.skill}: ${skill.growth_rate}% decline`); - }); - - demo.code("// Emerging roles"); - const emergingRoles = [ - "AI Engineer", - "DevSecOps Engineer", - "Data Engineer", - "Cloud Architect", - "Site Reliability Engineer", - ]; - - demo.success("Emerging roles:"); - emergingRoles.forEach((role) => { - demo.info(` ${role}`); - }); - - await waitForEnter(); -} - -async function demonstrateSkillAnalysis() { - demo.section("5. Skill Analysis"); - demo.info("The parser analyzes skill demand and salary correlation."); - - demo.code("// Skill demand analysis"); - logger.info("Analyzing skill demand..."); - await simulateProcessing(); - - const skillDemand = { - React: { count: 45, avg_salary: 98000 }, - Python: { count: 38, avg_salary: 102000 }, - AWS: { count: 32, avg_salary: 105000 }, - TypeScript: { count: 28, avg_salary: 95000 }, - Docker: { count: 25, avg_salary: 103000 }, - "Machine Learning": { count: 22, avg_salary: 115000 }, - }; - - demo.success("Top in-demand skills:"); - Object.entries(skillDemand) - .sort((a, b) => b[1].count - a[1].count) - .forEach(([skill, data]) => { - demo.info( - ` ${skill}: ${ - data.count - } jobs, avg salary $${data.avg_salary.toLocaleString()}` - ); - }); - - demo.code("// Salary correlation analysis"); - const salaryCorrelation = [ - { skill: "Machine Learning", correlation: 0.85 }, - { skill: "AWS", correlation: 0.78 }, - { skill: "Docker", correlation: 0.72 }, - { skill: "Python", correlation: 0.68 }, - { skill: "React", correlation: 0.65 }, - ]; - - demo.success("Skills with highest salary correlation:"); - salaryCorrelation.forEach((item) => { - demo.info( - ` ${item.skill}: ${(item.correlation * 100).toFixed(0)}% correlation` - ); - }); - - await waitForEnter(); -} - -async function demonstrateCompetitiveIntelligence() { - demo.section("6. Competitive Intelligence"); - demo.info( - "The parser provides insights into company hiring patterns and salary competitiveness." - ); - - demo.code("// Company hiring analysis"); - logger.info("Analyzing company hiring patterns..."); - await simulateProcessing(); - - const topHirers = [ - { company: "TechCorp", jobs: 25, avg_salary: 105000 }, - { company: "StartupXYZ", jobs: 18, avg_salary: 95000 }, - { company: "DataCorp", jobs: 15, avg_salary: 110000 }, - { company: "CloudTech", jobs: 12, avg_salary: 115000 }, - { company: "AI Solutions", jobs: 10, avg_salary: 120000 }, - ]; - - demo.success("Top hiring companies:"); - topHirers.forEach((company) => { - demo.info( - ` ${company.company}: ${ - company.jobs - } jobs, avg salary $${company.avg_salary.toLocaleString()}` - ); - }); - - demo.code("// Salary competitiveness"); - const salaryLeaders = [ - { company: "BigTech", avg_salary: 120000, market_position: "leader" }, - { company: "FinTech", avg_salary: 115000, market_position: "leader" }, - { company: "AI Solutions", avg_salary: 120000, market_position: "leader" }, - { - company: "StartupXYZ", - avg_salary: 95000, - market_position: "competitive", - }, - { company: "TechCorp", avg_salary: 105000, market_position: "competitive" }, - ]; - - demo.success("Salary leaders:"); - salaryLeaders.forEach((company) => { - const position = company.market_position === "leader" ? "šŸ†" : "šŸ“Š"; - demo.info( - ` ${position} ${ - company.company - }: $${company.avg_salary.toLocaleString()}` - ); - }); - - await waitForEnter(); -} - -async function demonstrateOutputGeneration() { - demo.section("7. Output Generation"); - demo.info( - "Results are saved in multiple formats with comprehensive analysis." - ); - - demo.code("// Generating comprehensive report"); - logger.file("Generating job market analysis report..."); - - const outputData = { - metadata: { - timestamp: new Date().toISOString(), - search_parameters: { - roles: ["software engineer", "data scientist", "frontend developer"], - locations: ["Toronto", "Vancouver", "Calgary"], - experience_levels: ["entry", "mid", "senior"], - remote_preference: ["remote", "hybrid", "onsite"], - }, - total_jobs_found: 1250, - analysis_duration_seconds: 45, - }, - market_overview: { - total_jobs: 1250, - average_salary: 95000, - salary_range: { min: 65000, max: 180000, median: 92000 }, - remote_distribution: { remote: 45, hybrid: 35, onsite: 20 }, - experience_distribution: { entry: 15, mid: 45, senior: 40 }, - }, - trends: { - growing_skills: [ - { skill: "React", growth_rate: 25 }, - { skill: "Python", growth_rate: 18 }, - { skill: "AWS", growth_rate: 22 }, - ], - declining_skills: [ - { skill: "jQuery", growth_rate: -12 }, - { skill: "PHP", growth_rate: -8 }, - ], - emerging_roles: ["AI Engineer", "DevSecOps Engineer", "Data Engineer"], - }, - jobs: mockJobs, - analysis: { - skill_demand: { - React: { count: 45, avg_salary: 98000 }, - Python: { count: 38, avg_salary: 102000 }, - AWS: { count: 32, avg_salary: 105000 }, - }, - company_insights: { - top_hirers: [ - { company: "TechCorp", jobs: 25 }, - { company: "StartupXYZ", jobs: 18 }, - ], - salary_leaders: [ - { company: "BigTech", avg_salary: 120000 }, - { company: "FinTech", avg_salary: 115000 }, - ], - }, - }, - }; - - // Save to demo file - const outputPath = path.join(__dirname, "demo-job-analysis.json"); - fs.writeFileSync(outputPath, JSON.stringify(outputData, null, 2)); - - demo.success(`Analysis report saved to: ${outputPath}`); - demo.info(`Total jobs analyzed: ${outputData.metadata.total_jobs_found}`); - demo.info( - `Analysis duration: ${outputData.metadata.analysis_duration_seconds} seconds` - ); - - demo.code("// Output formats available"); - demo.info("šŸ“ JSON: Comprehensive analysis with metadata"); - demo.info("šŸ“Š CSV: Tabular data for spreadsheet analysis"); - demo.info("šŸ“ˆ Charts: Visual trend analysis"); - demo.info("šŸ“‹ Summary: Executive summary report"); - - await waitForEnter(); -} - -// Helper functions -function waitForEnter() { - return new Promise((resolve) => { - const readline = require("readline"); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rl.question("\nPress Enter to continue...", () => { - rl.close(); - resolve(); - }); - }); -} - -async function simulateSearch() { - return new Promise((resolve) => { - const steps = [ - "Connecting to source", - "Searching jobs", - "Filtering results", - "Extracting data", - ]; - let i = 0; - const interval = setInterval(() => { - if (i < steps.length) { - logger.info(steps[i]); - i++; - } else { - clearInterval(interval); - resolve(); - } - }, 600); - }); -} - -async function simulateProcessing() { - return new Promise((resolve) => { - const dots = [".", "..", "..."]; - let i = 0; - const interval = setInterval(() => { - process.stdout.write(`\rProcessing${dots[i]}`); - i = (i + 1) % dots.length; - }, 500); - - setTimeout(() => { - clearInterval(interval); - process.stdout.write("\r"); - resolve(); - }, 2000); - }); -} - -// Run the demo if this file is executed directly -if (require.main === module) { - runDemo().catch((error) => { - demo.error(`Demo failed: ${error.message}`); - process.exit(1); - }); -} - -module.exports = { runDemo }; +/** + * Job Search Parser Demo + * + * Demonstrates the Job Search Parser's capabilities for job market intelligence, + * trend analysis, and competitive insights. + * + * This demo uses simulated data for demonstration purposes. + */ + +const { logger } = require("../ai-analyzer"); +const fs = require("fs"); +const path = require("path"); + +// Terminal colors for demo output +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + cyan: "\x1b[36m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + red: "\x1b[31m", +}; + +const demo = { + title: (text) => + console.log(`\n${colors.bright}${colors.cyan}${text}${colors.reset}`), + section: (text) => + console.log(`\n${colors.bright}${colors.magenta}${text}${colors.reset}`), + success: (text) => console.log(`${colors.green}āœ… ${text}${colors.reset}`), + info: (text) => console.log(`${colors.blue}ā„¹ļø ${text}${colors.reset}`), + warning: (text) => console.log(`${colors.yellow}āš ļø ${text}${colors.reset}`), + error: (text) => console.log(`${colors.red}āŒ ${text}${colors.reset}`), + code: (text) => console.log(`${colors.cyan}${text}${colors.reset}`), +}; + +// Mock job data for demonstration +const mockJobs = [ + { + id: "job_1", + title: "Senior Software Engineer", + company: "TechCorp", + location: "Toronto, Ontario", + remote_type: "hybrid", + salary: { min: 100000, max: 140000, currency: "CAD" }, + required_skills: ["React", "Node.js", "TypeScript", "AWS"], + preferred_skills: ["GraphQL", "Docker", "Kubernetes"], + experience_level: "senior", + job_url: "https://example.com/job/1", + posted_date: "2024-01-10T09:00:00Z", + scraped_at: "2024-01-15T10:30:00Z", + }, + { + id: "job_2", + title: "Data Scientist", + company: "DataCorp", + location: "Vancouver, British Columbia", + remote_type: "remote", + salary: { min: 90000, max: 130000, currency: "CAD" }, + required_skills: ["Python", "SQL", "Machine Learning", "Statistics"], + preferred_skills: ["TensorFlow", "PyTorch", "AWS"], + experience_level: "mid", + job_url: "https://example.com/job/2", + posted_date: "2024-01-09T14:30:00Z", + scraped_at: "2024-01-15T10:30:00Z", + }, + { + id: "job_3", + title: "Frontend Developer", + company: "StartupXYZ", + location: "Calgary, Alberta", + remote_type: "onsite", + salary: { min: 70000, max: 95000, currency: "CAD" }, + required_skills: ["React", "JavaScript", "CSS", "HTML"], + preferred_skills: ["Vue.js", "TypeScript", "Webpack"], + experience_level: "entry", + job_url: "https://example.com/job/3", + posted_date: "2024-01-08T11:15:00Z", + scraped_at: "2024-01-15T10:30:00Z", + }, + { + id: "job_4", + title: "DevOps Engineer", + company: "CloudTech", + location: "Toronto, Ontario", + remote_type: "hybrid", + salary: { min: 95000, max: 125000, currency: "CAD" }, + required_skills: ["Docker", "Kubernetes", "AWS", "Linux"], + preferred_skills: ["Terraform", "Jenkins", "Prometheus"], + experience_level: "senior", + job_url: "https://example.com/job/4", + posted_date: "2024-01-07T16:45:00Z", + scraped_at: "2024-01-15T10:30:00Z", + }, + { + id: "job_5", + title: "Machine Learning Engineer", + company: "AI Solutions", + location: "Vancouver, British Columbia", + remote_type: "remote", + salary: { min: 110000, max: 150000, currency: "CAD" }, + required_skills: ["Python", "TensorFlow", "PyTorch", "ML"], + preferred_skills: ["AWS", "Docker", "Kubernetes", "Spark"], + experience_level: "senior", + job_url: "https://example.com/job/5", + posted_date: "2024-01-06T10:20:00Z", + scraped_at: "2024-01-15T10:30:00Z", + }, +]; + +async function runDemo() { + demo.title("=== Job Search Parser Demo ==="); + demo.info( + "This demo showcases the Job Search Parser's capabilities for job market intelligence." + ); + demo.info("All data shown is simulated for demonstration purposes."); + demo.info("Press Enter to continue through each section...\n"); + + await waitForEnter(); + + // 1. Configuration Demo + await demonstrateConfiguration(); + + // 2. Job Search Process Demo + await demonstrateJobSearch(); + + // 3. Market Analysis Demo + await demonstrateMarketAnalysis(); + + // 4. Trend Analysis Demo + await demonstrateTrendAnalysis(); + + // 5. Skill Analysis Demo + await demonstrateSkillAnalysis(); + + // 6. Competitive Intelligence Demo + await demonstrateCompetitiveIntelligence(); + + // 7. Output Generation Demo + await demonstrateOutputGeneration(); + + demo.title("=== Demo Complete ==="); + demo.success("Job Search Parser demo completed successfully!"); + demo.info("Check the README.md for detailed usage instructions."); +} + +async function demonstrateConfiguration() { + demo.section("1. Configuration Setup"); + demo.info( + "The Job Search Parser uses environment variables and command-line options for configuration." + ); + + demo.code("// Environment Variables (.env file)"); + demo.info("SEARCH_SOURCES=linkedin,indeed,glassdoor"); + demo.info("TARGET_ROLES=software engineer,data scientist,product manager"); + demo.info("LOCATION_FILTER=Toronto,Vancouver,Calgary"); + demo.info("EXPERIENCE_LEVELS=entry,mid,senior"); + demo.info("REMOTE_PREFERENCE=remote,hybrid,onsite"); + demo.info("ENABLE_SALARY_ANALYSIS=true"); + demo.info("ENABLE_SKILL_ANALYSIS=true"); + demo.info("ENABLE_TREND_ANALYSIS=true"); + + demo.code("// Command Line Options"); + demo.info('node index.js --roles="frontend developer,backend developer"'); + demo.info('node index.js --locations="Toronto,Vancouver"'); + demo.info('node index.js --experience="senior" --salary-min=100000'); + demo.info('node index.js --remote="remote" --trends --skills'); + + await waitForEnter(); +} + +async function demonstrateJobSearch() { + demo.section("2. Job Search Process"); + demo.info( + "The parser searches multiple job platforms for relevant positions." + ); + + const searchSources = ["LinkedIn", "Indeed", "Glassdoor"]; + const targetRoles = [ + "Software Engineer", + "Data Scientist", + "Frontend Developer", + ]; + + demo.code("// Multi-source job search"); + for (const source of searchSources) { + logger.search(`Searching ${source} for job postings...`); + await simulateSearch(); + + const jobsFound = Math.floor(Math.random() * 200) + 50; + logger.success(`Found ${jobsFound} jobs on ${source}`); + } + + demo.code("// Role-specific filtering"); + for (const role of targetRoles) { + logger.info(`Filtering for ${role} positions...`); + await simulateProcessing(); + + const roleJobs = Math.floor(Math.random() * 30) + 10; + logger.success(`Found ${roleJobs} ${role} positions`); + } + + await waitForEnter(); +} + +async function demonstrateMarketAnalysis() { + demo.section("3. Market Analysis"); + demo.info( + "The parser analyzes market trends, salary ranges, and job distribution." + ); + + demo.code("// Market overview analysis"); + logger.info("Analyzing market overview..."); + await simulateProcessing(); + + const marketOverview = { + total_jobs: 1250, + average_salary: 95000, + salary_range: { min: 65000, max: 180000, median: 92000 }, + remote_distribution: { remote: 45, hybrid: 35, onsite: 20 }, + experience_distribution: { entry: 15, mid: 45, senior: 40 }, + }; + + demo.success(`Total jobs found: ${marketOverview.total_jobs}`); + demo.info( + `Average salary: $${marketOverview.average_salary.toLocaleString()}` + ); + demo.info( + `Salary range: $${marketOverview.salary_range.min.toLocaleString()} - $${marketOverview.salary_range.max.toLocaleString()}` + ); + demo.info( + `Remote work: ${marketOverview.remote_distribution.remote}% remote, ${marketOverview.remote_distribution.hybrid}% hybrid` + ); + + demo.code("// Geographic distribution"); + const locations = { + Toronto: 45, + Vancouver: 30, + Calgary: 15, + Other: 10, + }; + + Object.entries(locations).forEach(([location, percentage]) => { + demo.info(`${location}: ${percentage}% of jobs`); + }); + + await waitForEnter(); +} + +async function demonstrateTrendAnalysis() { + demo.section("4. Trend Analysis"); + demo.info( + "The parser identifies growing and declining skills and emerging roles." + ); + + demo.code("// Skill trend analysis"); + logger.info("Analyzing skill trends..."); + await simulateProcessing(); + + const growingSkills = [ + { skill: "React", growth_rate: 25 }, + { skill: "Python", growth_rate: 18 }, + { skill: "AWS", growth_rate: 22 }, + { skill: "TypeScript", growth_rate: 30 }, + { skill: "Docker", growth_rate: 15 }, + ]; + + const decliningSkills = [ + { skill: "jQuery", growth_rate: -12 }, + { skill: "PHP", growth_rate: -8 }, + { skill: "Angular", growth_rate: -5 }, + ]; + + demo.success("Growing skills:"); + growingSkills.forEach((skill) => { + demo.info(` ${skill.skill}: +${skill.growth_rate}% growth`); + }); + + demo.warning("Declining skills:"); + decliningSkills.forEach((skill) => { + demo.info(` ${skill.skill}: ${skill.growth_rate}% decline`); + }); + + demo.code("// Emerging roles"); + const emergingRoles = [ + "AI Engineer", + "DevSecOps Engineer", + "Data Engineer", + "Cloud Architect", + "Site Reliability Engineer", + ]; + + demo.success("Emerging roles:"); + emergingRoles.forEach((role) => { + demo.info(` ${role}`); + }); + + await waitForEnter(); +} + +async function demonstrateSkillAnalysis() { + demo.section("5. Skill Analysis"); + demo.info("The parser analyzes skill demand and salary correlation."); + + demo.code("// Skill demand analysis"); + logger.info("Analyzing skill demand..."); + await simulateProcessing(); + + const skillDemand = { + React: { count: 45, avg_salary: 98000 }, + Python: { count: 38, avg_salary: 102000 }, + AWS: { count: 32, avg_salary: 105000 }, + TypeScript: { count: 28, avg_salary: 95000 }, + Docker: { count: 25, avg_salary: 103000 }, + "Machine Learning": { count: 22, avg_salary: 115000 }, + }; + + demo.success("Top in-demand skills:"); + Object.entries(skillDemand) + .sort((a, b) => b[1].count - a[1].count) + .forEach(([skill, data]) => { + demo.info( + ` ${skill}: ${ + data.count + } jobs, avg salary $${data.avg_salary.toLocaleString()}` + ); + }); + + demo.code("// Salary correlation analysis"); + const salaryCorrelation = [ + { skill: "Machine Learning", correlation: 0.85 }, + { skill: "AWS", correlation: 0.78 }, + { skill: "Docker", correlation: 0.72 }, + { skill: "Python", correlation: 0.68 }, + { skill: "React", correlation: 0.65 }, + ]; + + demo.success("Skills with highest salary correlation:"); + salaryCorrelation.forEach((item) => { + demo.info( + ` ${item.skill}: ${(item.correlation * 100).toFixed(0)}% correlation` + ); + }); + + await waitForEnter(); +} + +async function demonstrateCompetitiveIntelligence() { + demo.section("6. Competitive Intelligence"); + demo.info( + "The parser provides insights into company hiring patterns and salary competitiveness." + ); + + demo.code("// Company hiring analysis"); + logger.info("Analyzing company hiring patterns..."); + await simulateProcessing(); + + const topHirers = [ + { company: "TechCorp", jobs: 25, avg_salary: 105000 }, + { company: "StartupXYZ", jobs: 18, avg_salary: 95000 }, + { company: "DataCorp", jobs: 15, avg_salary: 110000 }, + { company: "CloudTech", jobs: 12, avg_salary: 115000 }, + { company: "AI Solutions", jobs: 10, avg_salary: 120000 }, + ]; + + demo.success("Top hiring companies:"); + topHirers.forEach((company) => { + demo.info( + ` ${company.company}: ${ + company.jobs + } jobs, avg salary $${company.avg_salary.toLocaleString()}` + ); + }); + + demo.code("// Salary competitiveness"); + const salaryLeaders = [ + { company: "BigTech", avg_salary: 120000, market_position: "leader" }, + { company: "FinTech", avg_salary: 115000, market_position: "leader" }, + { company: "AI Solutions", avg_salary: 120000, market_position: "leader" }, + { + company: "StartupXYZ", + avg_salary: 95000, + market_position: "competitive", + }, + { company: "TechCorp", avg_salary: 105000, market_position: "competitive" }, + ]; + + demo.success("Salary leaders:"); + salaryLeaders.forEach((company) => { + const position = company.market_position === "leader" ? "šŸ†" : "šŸ“Š"; + demo.info( + ` ${position} ${ + company.company + }: $${company.avg_salary.toLocaleString()}` + ); + }); + + await waitForEnter(); +} + +async function demonstrateOutputGeneration() { + demo.section("7. Output Generation"); + demo.info( + "Results are saved in multiple formats with comprehensive analysis." + ); + + demo.code("// Generating comprehensive report"); + logger.file("Generating job market analysis report..."); + + const outputData = { + metadata: { + timestamp: new Date().toISOString(), + search_parameters: { + roles: ["software engineer", "data scientist", "frontend developer"], + locations: ["Toronto", "Vancouver", "Calgary"], + experience_levels: ["entry", "mid", "senior"], + remote_preference: ["remote", "hybrid", "onsite"], + }, + total_jobs_found: 1250, + analysis_duration_seconds: 45, + }, + market_overview: { + total_jobs: 1250, + average_salary: 95000, + salary_range: { min: 65000, max: 180000, median: 92000 }, + remote_distribution: { remote: 45, hybrid: 35, onsite: 20 }, + experience_distribution: { entry: 15, mid: 45, senior: 40 }, + }, + trends: { + growing_skills: [ + { skill: "React", growth_rate: 25 }, + { skill: "Python", growth_rate: 18 }, + { skill: "AWS", growth_rate: 22 }, + ], + declining_skills: [ + { skill: "jQuery", growth_rate: -12 }, + { skill: "PHP", growth_rate: -8 }, + ], + emerging_roles: ["AI Engineer", "DevSecOps Engineer", "Data Engineer"], + }, + jobs: mockJobs, + analysis: { + skill_demand: { + React: { count: 45, avg_salary: 98000 }, + Python: { count: 38, avg_salary: 102000 }, + AWS: { count: 32, avg_salary: 105000 }, + }, + company_insights: { + top_hirers: [ + { company: "TechCorp", jobs: 25 }, + { company: "StartupXYZ", jobs: 18 }, + ], + salary_leaders: [ + { company: "BigTech", avg_salary: 120000 }, + { company: "FinTech", avg_salary: 115000 }, + ], + }, + }, + }; + + // Save to demo file + const outputPath = path.join(__dirname, "demo-job-analysis.json"); + fs.writeFileSync(outputPath, JSON.stringify(outputData, null, 2)); + + demo.success(`Analysis report saved to: ${outputPath}`); + demo.info(`Total jobs analyzed: ${outputData.metadata.total_jobs_found}`); + demo.info( + `Analysis duration: ${outputData.metadata.analysis_duration_seconds} seconds` + ); + + demo.code("// Output formats available"); + demo.info("šŸ“ JSON: Comprehensive analysis with metadata"); + demo.info("šŸ“Š CSV: Tabular data for spreadsheet analysis"); + demo.info("šŸ“ˆ Charts: Visual trend analysis"); + demo.info("šŸ“‹ Summary: Executive summary report"); + + await waitForEnter(); +} + +// Helper functions +function waitForEnter() { + return new Promise((resolve) => { + const readline = require("readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.question("\nPress Enter to continue...", () => { + rl.close(); + resolve(); + }); + }); +} + +async function simulateSearch() { + return new Promise((resolve) => { + const steps = [ + "Connecting to source", + "Searching jobs", + "Filtering results", + "Extracting data", + ]; + let i = 0; + const interval = setInterval(() => { + if (i < steps.length) { + logger.info(steps[i]); + i++; + } else { + clearInterval(interval); + resolve(); + } + }, 600); + }); +} + +async function simulateProcessing() { + return new Promise((resolve) => { + const dots = [".", "..", "..."]; + let i = 0; + const interval = setInterval(() => { + process.stdout.write(`\rProcessing${dots[i]}`); + i = (i + 1) % dots.length; + }, 500); + + setTimeout(() => { + clearInterval(interval); + process.stdout.write("\r"); + resolve(); + }, 2000); + }); +} + +// Run the demo if this file is executed directly +if (require.main === module) { + runDemo().catch((error) => { + demo.error(`Demo failed: ${error.message}`); + process.exit(1); + }); +} + +module.exports = { runDemo }; diff --git a/job-search-parser/keywords/job-search-keywords.csv b/job-search-parser/keywords/job-search-keywords.csv index 23be162..df9714a 100644 --- a/job-search-parser/keywords/job-search-keywords.csv +++ b/job-search-parser/keywords/job-search-keywords.csv @@ -1,9 +1,9 @@ -keyword -qa automation -automation test -sdet -qa lead -automation lead -playwright -cypress +keyword +qa automation +automation test +sdet +qa lead +automation lead +playwright +cypress quality assurance engineer \ No newline at end of file diff --git a/job-search-parser/parsers/skipthedrive-demo.js b/job-search-parser/parsers/skipthedrive-demo.js index aafd8f3..23292a4 100644 --- a/job-search-parser/parsers/skipthedrive-demo.js +++ b/job-search-parser/parsers/skipthedrive-demo.js @@ -1,129 +1,129 @@ -#!/usr/bin/env node - -/** - * SkipTheDrive Parser Demo - * - * Demonstrates the SkipTheDrive job parser functionality - */ - -const { parseSkipTheDrive } = require("./skipthedrive"); -const fs = require("fs"); -const path = require("path"); -const { logger } = require("../../ai-analyzer"); - -// Load environment variables -require("dotenv").config({ path: path.join(__dirname, "..", ".env") }); - -async function runDemo() { - logger.step("šŸš€ SkipTheDrive Parser Demo"); - - // Demo configuration - const options = { - // Search for QA automation jobs (from your example) - keywords: process.env.SEARCH_KEYWORDS?.split(",").map((k) => k.trim()) || [ - "automation qa", - "qa engineer", - "test automation", - ], - - // Job type filters - can be: "part time", "full time", "contract" - jobTypes: process.env.JOB_TYPES?.split(",").map((t) => t.trim()) || [], - - // Location filter (optional) - locationFilter: process.env.LOCATION_FILTER || "", - - // Maximum pages to parse - maxPages: parseInt(process.env.MAX_PAGES) || 3, - - // Browser headless mode - headless: process.env.HEADLESS !== "false", - - // AI analysis - enableAI: process.env.ENABLE_AI_ANALYSIS !== "false", - aiContext: "remote QA and test automation job opportunities", - }; - - logger.info("Configuration:"); - logger.info(`- Keywords: ${options.keywords.join(", ")}`); - logger.info( - `- Job Types: ${ - options.jobTypes.length > 0 ? options.jobTypes.join(", ") : "All types" - }` - ); - logger.info(`- Location Filter: ${options.locationFilter || "None"}`); - logger.info(`- Max Pages: ${options.maxPages}`); - logger.info(`- Headless: ${options.headless}`); - logger.info(`- AI Analysis: ${options.enableAI}`); - logger.info("\nStarting parser..."); - - try { - const startTime = Date.now(); - const results = await parseSkipTheDrive(options); - const duration = ((Date.now() - startTime) / 1000).toFixed(2); - - // Save results - const timestamp = new Date() - .toISOString() - .replace(/[:.]/g, "-") - .slice(0, -5); - const resultsDir = path.join(__dirname, "..", "results"); - - if (!fs.existsSync(resultsDir)) { - fs.mkdirSync(resultsDir, { recursive: true }); - } - - const resultsFile = path.join( - resultsDir, - `skipthedrive-results-${timestamp}.json` - ); - fs.writeFileSync(resultsFile, JSON.stringify(results, null, 2)); - - // Display summary - logger.step("\nšŸ“Š Parsing Summary:"); - logger.info(`- Duration: ${duration} seconds`); - logger.info(`- Jobs Found: ${results.results.length}`); - logger.info(`- Jobs Rejected: ${results.rejectedResults.length}`); - logger.file(`- Results saved to: ${resultsFile}`); - - // Show sample results - if (results.results.length > 0) { - logger.info("\nšŸ” Sample Jobs Found:"); - results.results.slice(0, 5).forEach((job, index) => { - logger.info(`\n${index + 1}. ${job.title}`); - logger.info(` Company: ${job.company}`); - logger.info(` Posted: ${job.daysAgo}`); - logger.info(` Featured: ${job.isFeatured ? "Yes" : "No"}`); - logger.info(` URL: ${job.jobUrl}`); - if (job.aiAnalysis) { - logger.ai( - ` AI Relevant: ${job.aiAnalysis.isRelevant ? "Yes" : "No"} (${( - job.aiAnalysis.confidence * 100 - ).toFixed(0)}% confidence)` - ); - } - }); - } - - // Show rejection reasons - if (results.rejectedResults.length > 0) { - const rejectionReasons = {}; - results.rejectedResults.forEach((job) => { - rejectionReasons[job.reason] = (rejectionReasons[job.reason] || 0) + 1; - }); - - logger.info("\nāŒ Rejection Reasons:"); - Object.entries(rejectionReasons).forEach(([reason, count]) => { - logger.info(` ${reason}: ${count}`); - }); - } - } catch (error) { - logger.error("\nāŒ Demo failed:", error.message); - process.exit(1); - } -} - -// Run the demo -runDemo().catch((err) => { - logger.error("Fatal error:", err); - process.exit(1); -}); +#!/usr/bin/env node + +/** + * SkipTheDrive Parser Demo + * + * Demonstrates the SkipTheDrive job parser functionality + */ + +const { parseSkipTheDrive } = require("./skipthedrive"); +const fs = require("fs"); +const path = require("path"); +const { logger } = require("../../ai-analyzer"); + +// Load environment variables +require("dotenv").config({ path: path.join(__dirname, "..", ".env") }); + +async function runDemo() { + logger.step("šŸš€ SkipTheDrive Parser Demo"); + + // Demo configuration + const options = { + // Search for QA automation jobs (from your example) + keywords: process.env.SEARCH_KEYWORDS?.split(",").map((k) => k.trim()) || [ + "automation qa", + "qa engineer", + "test automation", + ], + + // Job type filters - can be: "part time", "full time", "contract" + jobTypes: process.env.JOB_TYPES?.split(",").map((t) => t.trim()) || [], + + // Location filter (optional) + locationFilter: process.env.LOCATION_FILTER || "", + + // Maximum pages to parse + maxPages: parseInt(process.env.MAX_PAGES) || 3, + + // Browser headless mode + headless: process.env.HEADLESS !== "false", + + // AI analysis + enableAI: process.env.ENABLE_AI_ANALYSIS !== "false", + aiContext: "remote QA and test automation job opportunities", + }; + + logger.info("Configuration:"); + logger.info(`- Keywords: ${options.keywords.join(", ")}`); + logger.info( + `- Job Types: ${ + options.jobTypes.length > 0 ? options.jobTypes.join(", ") : "All types" + }` + ); + logger.info(`- Location Filter: ${options.locationFilter || "None"}`); + logger.info(`- Max Pages: ${options.maxPages}`); + logger.info(`- Headless: ${options.headless}`); + logger.info(`- AI Analysis: ${options.enableAI}`); + logger.info("\nStarting parser..."); + + try { + const startTime = Date.now(); + const results = await parseSkipTheDrive(options); + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + + // Save results + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, "-") + .slice(0, -5); + const resultsDir = path.join(__dirname, "..", "results"); + + if (!fs.existsSync(resultsDir)) { + fs.mkdirSync(resultsDir, { recursive: true }); + } + + const resultsFile = path.join( + resultsDir, + `skipthedrive-results-${timestamp}.json` + ); + fs.writeFileSync(resultsFile, JSON.stringify(results, null, 2)); + + // Display summary + logger.step("\nšŸ“Š Parsing Summary:"); + logger.info(`- Duration: ${duration} seconds`); + logger.info(`- Jobs Found: ${results.results.length}`); + logger.info(`- Jobs Rejected: ${results.rejectedResults.length}`); + logger.file(`- Results saved to: ${resultsFile}`); + + // Show sample results + if (results.results.length > 0) { + logger.info("\nšŸ” Sample Jobs Found:"); + results.results.slice(0, 5).forEach((job, index) => { + logger.info(`\n${index + 1}. ${job.title}`); + logger.info(` Company: ${job.company}`); + logger.info(` Posted: ${job.daysAgo}`); + logger.info(` Featured: ${job.isFeatured ? "Yes" : "No"}`); + logger.info(` URL: ${job.jobUrl}`); + if (job.aiAnalysis) { + logger.ai( + ` AI Relevant: ${job.aiAnalysis.isRelevant ? "Yes" : "No"} (${( + job.aiAnalysis.confidence * 100 + ).toFixed(0)}% confidence)` + ); + } + }); + } + + // Show rejection reasons + if (results.rejectedResults.length > 0) { + const rejectionReasons = {}; + results.rejectedResults.forEach((job) => { + rejectionReasons[job.reason] = (rejectionReasons[job.reason] || 0) + 1; + }); + + logger.info("\nāŒ Rejection Reasons:"); + Object.entries(rejectionReasons).forEach(([reason, count]) => { + logger.info(` ${reason}: ${count}`); + }); + } + } catch (error) { + logger.error("\nāŒ Demo failed:", error.message); + process.exit(1); + } +} + +// Run the demo +runDemo().catch((err) => { + logger.error("Fatal error:", err); + process.exit(1); +}); diff --git a/job-search-parser/parsers/skipthedrive.js b/job-search-parser/parsers/skipthedrive.js index 134ea52..1328c94 100644 --- a/job-search-parser/parsers/skipthedrive.js +++ b/job-search-parser/parsers/skipthedrive.js @@ -1,332 +1,332 @@ -/** - * SkipTheDrive Job Parser - * - * Parses remote job listings from SkipTheDrive.com - * Supports keyword search, job type filters, and pagination - */ - -const { chromium } = require("playwright"); -const path = require("path"); - -// Import from ai-analyzer core package -const { - logger, - cleanText, - containsAnyKeyword, - parseLocationFilters, - validateLocationAgainstFilters, - extractLocationFromProfile, - analyzeBatch, - checkOllamaStatus, -} = require("../../ai-analyzer"); - -/** - * Build search URL for SkipTheDrive - * @param {string} keyword - Search keyword - * @param {string} orderBy - Sort order (date, relevance) - * @param {Array} jobTypes - Job types to filter (part time, full time, contract) - * @returns {string} - Formatted search URL - */ -function buildSearchUrl(keyword, orderBy = "date", jobTypes = []) { - let url = `https://www.skipthedrive.com/?s=${encodeURIComponent(keyword)}`; - - if (orderBy) { - url += `&orderby=${orderBy}`; - } - - // Add job type filters - jobTypes.forEach((type) => { - url += `&jobtype=${encodeURIComponent(type)}`; - }); - - return url; -} - -/** - * Extract job data from a single job listing element - * @param {Element} article - Job listing DOM element - * @returns {Object} - Extracted job data - */ -async function extractJobData(article) { - try { - // Extract job title and URL - const titleElement = await article.$("h2.post-title a"); - const title = titleElement ? await titleElement.textContent() : ""; - const jobUrl = titleElement ? await titleElement.getAttribute("href") : ""; - - // Extract date - const dateElement = await article.$("time.post-date"); - const datePosted = dateElement - ? await dateElement.getAttribute("datetime") - : ""; - const dateText = dateElement ? await dateElement.textContent() : ""; - - // Extract company name - const companyElement = await article.$( - ".custom_fields_company_name_display_search_results" - ); - let company = companyElement ? await companyElement.textContent() : ""; - company = company.replace(/^\s*[^\s]+\s*/, "").trim(); // Remove icon - - // Extract days ago - const daysAgoElement = await article.$( - ".custom_fields_job_date_display_search_results" - ); - let daysAgo = daysAgoElement ? await daysAgoElement.textContent() : ""; - daysAgo = daysAgo.replace(/^\s*[^\s]+\s*/, "").trim(); // Remove icon - - // Extract job description excerpt - const excerptElement = await article.$(".excerpt_part"); - const description = excerptElement - ? await excerptElement.textContent() - : ""; - - // Check if featured/sponsored - const featuredElement = await article.$(".custom_fields_sponsored_job"); - const isFeatured = !!featuredElement; - - // Extract job ID from article ID - const articleId = await article.getAttribute("id"); - const jobId = articleId ? articleId.replace("post-", "") : ""; - - return { - jobId, - title: cleanText(title), - company: cleanText(company), - jobUrl, - datePosted, - dateText: cleanText(dateText), - daysAgo: cleanText(daysAgo), - description: cleanText(description), - isFeatured, - source: "skipthedrive", - timestamp: new Date().toISOString(), - }; - } catch (error) { - logger.error(`Error extracting job data: ${error.message}`); - return null; - } -} - -/** - * Parse SkipTheDrive job listings - * @param {Object} options - Parser options - * @returns {Promise} - Array of parsed job listings - */ -async function parseSkipTheDrive(options = {}) { - const { - keywords = process.env.SEARCH_KEYWORDS?.split(",").map((k) => k.trim()) || [ - "software engineer", - "developer", - ], - jobTypes = process.env.JOB_TYPES?.split(",").map((t) => t.trim()) || [], - locationFilter = process.env.LOCATION_FILTER || "", - maxPages = parseInt(process.env.MAX_PAGES) || 5, - headless = process.env.HEADLESS !== "false", - enableAI = process.env.ENABLE_AI_ANALYSIS === "true", - aiContext = process.env.AI_CONTEXT || "remote job opportunities analysis", - } = options; - - logger.step("Starting SkipTheDrive parser..."); - logger.info(`šŸ” Keywords: ${keywords.join(", ")}`); - logger.info( - `šŸ“‹ Job Types: ${jobTypes.length > 0 ? jobTypes.join(", ") : "All"}` - ); - logger.info(`šŸ“ Location Filter: ${locationFilter || "None"}`); - logger.info(`šŸ“„ Max Pages: ${maxPages}`); - - const browser = await chromium.launch({ - headless, - args: [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - ], - }); - - const context = await browser.newContext({ - userAgent: - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", - }); - - const results = []; - const rejectedResults = []; - const seenJobs = new Set(); - - try { - // Search for each keyword - for (const keyword of keywords) { - logger.info(`\nšŸ” Searching for: ${keyword}`); - - const searchUrl = buildSearchUrl(keyword, "date", jobTypes); - const page = await context.newPage(); - - try { - logger.info( - `Attempting navigation to: ${searchUrl} at ${new Date().toISOString()}` - ); - await page.goto(searchUrl, { - waitUntil: "domcontentloaded", - timeout: 30000, - }); - logger.info( - `Navigation completed successfully at ${new Date().toISOString()}` - ); - - // Wait for job listings to load - logger.info("Waiting for selector #loops-wrapper"); - await page - .waitForSelector("#loops-wrapper", { timeout: 5000 }) - .catch(() => { - logger.warning(`No results found for keyword: ${keyword}`); - }); - logger.info("Selector wait completed"); - - let currentPage = 1; - let hasNextPage = true; - - while (hasNextPage && currentPage <= maxPages) { - logger.info(`šŸ“„ Processing page ${currentPage} for "${keyword}"`); - - // Extract all job articles on current page - const jobArticles = await page.$$("article[id^='post-']"); - logger.info( - `Found ${jobArticles.length} job listings on page ${currentPage}` - ); - - for (const article of jobArticles) { - const jobData = await extractJobData(article); - - if (!jobData || seenJobs.has(jobData.jobId)) { - continue; - } - - seenJobs.add(jobData.jobId); - - // Add keyword that found this job - jobData.searchKeyword = keyword; - - // Validate job against keywords - const fullText = `${jobData.title} ${jobData.description} ${jobData.company}`; - if (!containsAnyKeyword(fullText, keywords)) { - rejectedResults.push({ - ...jobData, - rejected: true, - reason: "Keywords not found in job listing", - }); - continue; - } - - // Location validation (if enabled) - if (locationFilter) { - const locationFilters = parseLocationFilters(locationFilter); - // For SkipTheDrive, most jobs are remote, but we can check the title/description - const locationValid = - fullText.toLowerCase().includes("remote") || - locationFilters.some((filter) => - fullText.toLowerCase().includes(filter.toLowerCase()) - ); - - if (!locationValid) { - rejectedResults.push({ - ...jobData, - rejected: true, - reason: "Location requirements not met", - }); - continue; - } - - jobData.locationValid = locationValid; - } - - logger.success(`āœ… Found: ${jobData.title} at ${jobData.company}`); - results.push(jobData); - } - - // Check for next page - const nextPageLink = await page.$("a.nextp"); - if (nextPageLink && currentPage < maxPages) { - logger.info("šŸ“„ Moving to next page..."); - await nextPageLink.click(); - await page.waitForLoadState("domcontentloaded"); - await page.waitForTimeout(2000); // Wait for content to load - currentPage++; - } else { - hasNextPage = false; - } - } - } catch (error) { - logger.error(`Error processing keyword "${keyword}": ${error.message}`); - } finally { - await page.close(); - } - } - - logger.success(`\nāœ… Parsing complete!`); - logger.info(`šŸ“Š Total jobs found: ${results.length}`); - logger.info(`āŒ Rejected jobs: ${rejectedResults.length}`); - - // Run AI analysis if enabled - let aiAnalysis = null; - if (enableAI && results.length > 0) { - logger.step("Running AI analysis on job listings..."); - - const aiAvailable = await checkOllamaStatus(); - if (aiAvailable) { - const analysisData = results.map((job) => ({ - text: `${job.title} at ${job.company}. ${job.description}`, - metadata: { - jobId: job.jobId, - company: job.company, - daysAgo: job.daysAgo, - }, - })); - - aiAnalysis = await analyzeBatch(analysisData, aiContext); - - // Merge AI analysis with results - results.forEach((job, index) => { - if (aiAnalysis && aiAnalysis[index]) { - job.aiAnalysis = { - isRelevant: aiAnalysis[index].isRelevant, - confidence: aiAnalysis[index].confidence, - reasoning: aiAnalysis[index].reasoning, - }; - } - }); - - logger.success("āœ… AI analysis completed"); - } else { - logger.warning("āš ļø AI not available - skipping analysis"); - } - } - - return { - results, - rejectedResults, - metadata: { - source: "skipthedrive", - totalJobs: results.length, - rejectedJobs: rejectedResults.length, - keywords: keywords, - jobTypes: jobTypes, - locationFilter: locationFilter, - aiAnalysisEnabled: enableAI, - aiAnalysisCompleted: !!aiAnalysis, - timestamp: new Date().toISOString(), - }, - }; - } catch (error) { - logger.error(`Fatal error in SkipTheDrive parser: ${error.message}`); - throw error; - } finally { - await browser.close(); - } -} - -// Export the parser -module.exports = { - parseSkipTheDrive, - buildSearchUrl, - extractJobData, -}; +/** + * SkipTheDrive Job Parser + * + * Parses remote job listings from SkipTheDrive.com + * Supports keyword search, job type filters, and pagination + */ + +const { chromium } = require("playwright"); +const path = require("path"); + +// Import from ai-analyzer core package +const { + logger, + cleanText, + containsAnyKeyword, + parseLocationFilters, + validateLocationAgainstFilters, + extractLocationFromProfile, + analyzeBatch, + checkOllamaStatus, +} = require("../../ai-analyzer"); + +/** + * Build search URL for SkipTheDrive + * @param {string} keyword - Search keyword + * @param {string} orderBy - Sort order (date, relevance) + * @param {Array} jobTypes - Job types to filter (part time, full time, contract) + * @returns {string} - Formatted search URL + */ +function buildSearchUrl(keyword, orderBy = "date", jobTypes = []) { + let url = `https://www.skipthedrive.com/?s=${encodeURIComponent(keyword)}`; + + if (orderBy) { + url += `&orderby=${orderBy}`; + } + + // Add job type filters + jobTypes.forEach((type) => { + url += `&jobtype=${encodeURIComponent(type)}`; + }); + + return url; +} + +/** + * Extract job data from a single job listing element + * @param {Element} article - Job listing DOM element + * @returns {Object} - Extracted job data + */ +async function extractJobData(article) { + try { + // Extract job title and URL + const titleElement = await article.$("h2.post-title a"); + const title = titleElement ? await titleElement.textContent() : ""; + const jobUrl = titleElement ? await titleElement.getAttribute("href") : ""; + + // Extract date + const dateElement = await article.$("time.post-date"); + const datePosted = dateElement + ? await dateElement.getAttribute("datetime") + : ""; + const dateText = dateElement ? await dateElement.textContent() : ""; + + // Extract company name + const companyElement = await article.$( + ".custom_fields_company_name_display_search_results" + ); + let company = companyElement ? await companyElement.textContent() : ""; + company = company.replace(/^\s*[^\s]+\s*/, "").trim(); // Remove icon + + // Extract days ago + const daysAgoElement = await article.$( + ".custom_fields_job_date_display_search_results" + ); + let daysAgo = daysAgoElement ? await daysAgoElement.textContent() : ""; + daysAgo = daysAgo.replace(/^\s*[^\s]+\s*/, "").trim(); // Remove icon + + // Extract job description excerpt + const excerptElement = await article.$(".excerpt_part"); + const description = excerptElement + ? await excerptElement.textContent() + : ""; + + // Check if featured/sponsored + const featuredElement = await article.$(".custom_fields_sponsored_job"); + const isFeatured = !!featuredElement; + + // Extract job ID from article ID + const articleId = await article.getAttribute("id"); + const jobId = articleId ? articleId.replace("post-", "") : ""; + + return { + jobId, + title: cleanText(title), + company: cleanText(company), + jobUrl, + datePosted, + dateText: cleanText(dateText), + daysAgo: cleanText(daysAgo), + description: cleanText(description), + isFeatured, + source: "skipthedrive", + timestamp: new Date().toISOString(), + }; + } catch (error) { + logger.error(`Error extracting job data: ${error.message}`); + return null; + } +} + +/** + * Parse SkipTheDrive job listings + * @param {Object} options - Parser options + * @returns {Promise} - Array of parsed job listings + */ +async function parseSkipTheDrive(options = {}) { + const { + keywords = process.env.SEARCH_KEYWORDS?.split(",").map((k) => k.trim()) || [ + "software engineer", + "developer", + ], + jobTypes = process.env.JOB_TYPES?.split(",").map((t) => t.trim()) || [], + locationFilter = process.env.LOCATION_FILTER || "", + maxPages = parseInt(process.env.MAX_PAGES) || 5, + headless = process.env.HEADLESS !== "false", + enableAI = process.env.ENABLE_AI_ANALYSIS === "true", + aiContext = process.env.AI_CONTEXT || "remote job opportunities analysis", + } = options; + + logger.step("Starting SkipTheDrive parser..."); + logger.info(`šŸ” Keywords: ${keywords.join(", ")}`); + logger.info( + `šŸ“‹ Job Types: ${jobTypes.length > 0 ? jobTypes.join(", ") : "All"}` + ); + logger.info(`šŸ“ Location Filter: ${locationFilter || "None"}`); + logger.info(`šŸ“„ Max Pages: ${maxPages}`); + + const browser = await chromium.launch({ + headless, + args: [ + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + ], + }); + + const context = await browser.newContext({ + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + }); + + const results = []; + const rejectedResults = []; + const seenJobs = new Set(); + + try { + // Search for each keyword + for (const keyword of keywords) { + logger.info(`\nšŸ” Searching for: ${keyword}`); + + const searchUrl = buildSearchUrl(keyword, "date", jobTypes); + const page = await context.newPage(); + + try { + logger.info( + `Attempting navigation to: ${searchUrl} at ${new Date().toISOString()}` + ); + await page.goto(searchUrl, { + waitUntil: "domcontentloaded", + timeout: 30000, + }); + logger.info( + `Navigation completed successfully at ${new Date().toISOString()}` + ); + + // Wait for job listings to load + logger.info("Waiting for selector #loops-wrapper"); + await page + .waitForSelector("#loops-wrapper", { timeout: 5000 }) + .catch(() => { + logger.warning(`No results found for keyword: ${keyword}`); + }); + logger.info("Selector wait completed"); + + let currentPage = 1; + let hasNextPage = true; + + while (hasNextPage && currentPage <= maxPages) { + logger.info(`šŸ“„ Processing page ${currentPage} for "${keyword}"`); + + // Extract all job articles on current page + const jobArticles = await page.$$("article[id^='post-']"); + logger.info( + `Found ${jobArticles.length} job listings on page ${currentPage}` + ); + + for (const article of jobArticles) { + const jobData = await extractJobData(article); + + if (!jobData || seenJobs.has(jobData.jobId)) { + continue; + } + + seenJobs.add(jobData.jobId); + + // Add keyword that found this job + jobData.searchKeyword = keyword; + + // Validate job against keywords + const fullText = `${jobData.title} ${jobData.description} ${jobData.company}`; + if (!containsAnyKeyword(fullText, keywords)) { + rejectedResults.push({ + ...jobData, + rejected: true, + reason: "Keywords not found in job listing", + }); + continue; + } + + // Location validation (if enabled) + if (locationFilter) { + const locationFilters = parseLocationFilters(locationFilter); + // For SkipTheDrive, most jobs are remote, but we can check the title/description + const locationValid = + fullText.toLowerCase().includes("remote") || + locationFilters.some((filter) => + fullText.toLowerCase().includes(filter.toLowerCase()) + ); + + if (!locationValid) { + rejectedResults.push({ + ...jobData, + rejected: true, + reason: "Location requirements not met", + }); + continue; + } + + jobData.locationValid = locationValid; + } + + logger.success(`āœ… Found: ${jobData.title} at ${jobData.company}`); + results.push(jobData); + } + + // Check for next page + const nextPageLink = await page.$("a.nextp"); + if (nextPageLink && currentPage < maxPages) { + logger.info("šŸ“„ Moving to next page..."); + await nextPageLink.click(); + await page.waitForLoadState("domcontentloaded"); + await page.waitForTimeout(2000); // Wait for content to load + currentPage++; + } else { + hasNextPage = false; + } + } + } catch (error) { + logger.error(`Error processing keyword "${keyword}": ${error.message}`); + } finally { + await page.close(); + } + } + + logger.success(`\nāœ… Parsing complete!`); + logger.info(`šŸ“Š Total jobs found: ${results.length}`); + logger.info(`āŒ Rejected jobs: ${rejectedResults.length}`); + + // Run AI analysis if enabled + let aiAnalysis = null; + if (enableAI && results.length > 0) { + logger.step("Running AI analysis on job listings..."); + + const aiAvailable = await checkOllamaStatus(); + if (aiAvailable) { + const analysisData = results.map((job) => ({ + text: `${job.title} at ${job.company}. ${job.description}`, + metadata: { + jobId: job.jobId, + company: job.company, + daysAgo: job.daysAgo, + }, + })); + + aiAnalysis = await analyzeBatch(analysisData, aiContext); + + // Merge AI analysis with results + results.forEach((job, index) => { + if (aiAnalysis && aiAnalysis[index]) { + job.aiAnalysis = { + isRelevant: aiAnalysis[index].isRelevant, + confidence: aiAnalysis[index].confidence, + reasoning: aiAnalysis[index].reasoning, + }; + } + }); + + logger.success("āœ… AI analysis completed"); + } else { + logger.warning("āš ļø AI not available - skipping analysis"); + } + } + + return { + results, + rejectedResults, + metadata: { + source: "skipthedrive", + totalJobs: results.length, + rejectedJobs: rejectedResults.length, + keywords: keywords, + jobTypes: jobTypes, + locationFilter: locationFilter, + aiAnalysisEnabled: enableAI, + aiAnalysisCompleted: !!aiAnalysis, + timestamp: new Date().toISOString(), + }, + }; + } catch (error) { + logger.error(`Fatal error in SkipTheDrive parser: ${error.message}`); + throw error; + } finally { + await browser.close(); + } +} + +// Export the parser +module.exports = { + parseSkipTheDrive, + buildSearchUrl, + extractJobData, +}; diff --git a/job-search-parser/strategies/skipthedrive-strategy.js b/job-search-parser/strategies/skipthedrive-strategy.js index b3c36e4..5b39c7e 100644 --- a/job-search-parser/strategies/skipthedrive-strategy.js +++ b/job-search-parser/strategies/skipthedrive-strategy.js @@ -1,302 +1,302 @@ -/** - * SkipTheDrive Parsing Strategy - * - * Uses core-parser for browser management and ai-analyzer for utilities - */ - -const { - logger, - cleanText, - containsAnyKeyword, - validateLocationAgainstFilters, -} = require("ai-analyzer"); - -/** - * SkipTheDrive URL builder - */ -function buildSearchUrl(keyword, orderBy = "date", jobTypes = []) { - const baseUrl = "https://www.skipthedrive.com/"; - const params = new URLSearchParams({ - s: keyword, - orderby: orderBy, - }); - - if (jobTypes && jobTypes.length > 0) { - params.append("job_type", jobTypes.join(",")); - } - - return `${baseUrl}?${params.toString()}`; -} - -/** - * SkipTheDrive parsing strategy function - */ -async function skipthedriveStrategy(coreParser, options = {}) { - const { - keywords = ["software engineer", "developer", "programmer"], - locationFilter = null, - maxPages = 5, - jobTypes = [], - } = options; - - const results = []; - const rejectedResults = []; - const seenJobs = new Set(); - - try { - // Create main page - const page = await coreParser.createPage("skipthedrive-main"); - - logger.info("šŸš€ Starting SkipTheDrive parser..."); - logger.info(`šŸ” Keywords: ${keywords.join(", ")}`); - logger.info(`šŸ“ Location Filter: ${locationFilter || "None"}`); - logger.info(`šŸ“„ Max Pages: ${maxPages}`); - - // Search for each keyword - for (const keyword of keywords) { - logger.info(`\nšŸ” Searching for: ${keyword}`); - - const searchUrl = buildSearchUrl(keyword, "date", jobTypes); - - try { - // Navigate to search results - await coreParser.navigateTo(searchUrl, { - pageId: "skipthedrive-main", - retries: 2, - timeout: 30000, - }); - - // Wait for job listings to load - const hasResults = await coreParser - .waitForSelector( - "#loops-wrapper", - { - timeout: 5000, - }, - "skipthedrive-main" - ) - .catch(() => { - logger.warning(`No results found for keyword: ${keyword}`); - return false; - }); - - if (!hasResults) { - continue; - } - - // Process multiple pages - let currentPage = 1; - let hasNextPage = true; - - while (hasNextPage && currentPage <= maxPages) { - logger.info(`šŸ“„ Processing page ${currentPage} for "${keyword}"`); - - // Extract jobs from current page - const pageJobs = await extractJobsFromPage( - page, - keyword, - locationFilter - ); - - for (const job of pageJobs) { - // Skip duplicates - if (seenJobs.has(job.jobId)) continue; - seenJobs.add(job.jobId); - - // Validate location if filtering enabled - if (locationFilter) { - const locationValid = validateLocationAgainstFilters( - job.location, - locationFilter - ); - - if (!locationValid) { - rejectedResults.push({ - ...job, - rejectionReason: "Location filter mismatch", - }); - continue; - } - } - - results.push(job); - } - - // Check for next page - hasNextPage = await hasNextPageAvailable(page); - if (hasNextPage && currentPage < maxPages) { - await navigateToNextPage(page, currentPage + 1); - currentPage++; - - // Wait for new page to load - await page.waitForTimeout(2000); - } else { - hasNextPage = false; - } - } - } catch (error) { - logger.error(`Error processing keyword "${keyword}": ${error.message}`); - } - } - - logger.info( - `šŸŽÆ SkipTheDrive parsing completed: ${results.length} jobs found, ${rejectedResults.length} rejected` - ); - - return { - results, - rejectedResults, - summary: { - totalJobs: results.length, - totalRejected: rejectedResults.length, - keywords: keywords.join(", "), - locationFilter, - source: "skipthedrive", - }, - }; - } catch (error) { - logger.error(`āŒ SkipTheDrive parsing failed: ${error.message}`); - throw error; - } -} - -/** - * Extract jobs from current page - */ -async function extractJobsFromPage(page, keyword, locationFilter) { - const jobs = []; - - try { - // Get all job article elements - const jobElements = await page.$$("article.job_listing"); - - for (const jobElement of jobElements) { - try { - const job = await extractJobData(jobElement, keyword); - if (job) { - jobs.push(job); - } - } catch (error) { - logger.warning(`Failed to extract job data: ${error.message}`); - } - } - } catch (error) { - logger.error(`Failed to extract jobs from page: ${error.message}`); - } - - return jobs; -} - -/** - * Extract data from individual job element - */ -async function extractJobData(jobElement, keyword) { - try { - // Extract job ID - const articleId = (await jobElement.getAttribute("id")) || ""; - const jobId = articleId ? articleId.replace("post-", "") : ""; - - // Extract title - const titleElement = await jobElement.$(".job_listing-title a"); - const title = titleElement - ? cleanText(await titleElement.textContent()) - : ""; - const jobUrl = titleElement ? await titleElement.getAttribute("href") : ""; - - // Extract company - const companyElement = await jobElement.$(".company"); - const company = companyElement - ? cleanText(await companyElement.textContent()) - : ""; - - // Extract location - const locationElement = await jobElement.$(".location"); - const location = locationElement - ? cleanText(await locationElement.textContent()) - : ""; - - // Extract date posted - const dateElement = await jobElement.$(".job-date"); - const dateText = dateElement - ? cleanText(await dateElement.textContent()) - : ""; - - // Extract description - const descElement = await jobElement.$(".job_listing-description"); - const description = descElement - ? cleanText(await descElement.textContent()) - : ""; - - // Check if featured - const featuredElement = await jobElement.$(".featured"); - const isFeatured = featuredElement !== null; - - // Parse date - let datePosted = null; - let daysAgo = null; - - if (dateText) { - const match = dateText.match(/(\d+)\s+days?\s+ago/); - if (match) { - daysAgo = parseInt(match[1]); - const date = new Date(); - date.setDate(date.getDate() - daysAgo); - datePosted = date.toISOString().split("T")[0]; - } - } - - return { - jobId, - title, - company, - location, - jobUrl, - datePosted, - dateText, - daysAgo, - description, - isFeatured, - keyword, - extractedAt: new Date().toISOString(), - source: "skipthedrive", - }; - } catch (error) { - logger.warning(`Error extracting job data: ${error.message}`); - return null; - } -} - -/** - * Check if next page is available - */ -async function hasNextPageAvailable(page) { - try { - const nextButton = await page.$(".next-page"); - return nextButton !== null; - } catch { - return false; - } -} - -/** - * Navigate to next page - */ -async function navigateToNextPage(page, pageNumber) { - try { - const nextButton = await page.$(".next-page"); - if (nextButton) { - await nextButton.click(); - } - } catch (error) { - logger.warning( - `Failed to navigate to page ${pageNumber}: ${error.message}` - ); - } -} - -module.exports = { - skipthedriveStrategy, - buildSearchUrl, - extractJobsFromPage, - extractJobData, -}; +/** + * SkipTheDrive Parsing Strategy + * + * Uses core-parser for browser management and ai-analyzer for utilities + */ + +const { + logger, + cleanText, + containsAnyKeyword, + validateLocationAgainstFilters, +} = require("ai-analyzer"); + +/** + * SkipTheDrive URL builder + */ +function buildSearchUrl(keyword, orderBy = "date", jobTypes = []) { + const baseUrl = "https://www.skipthedrive.com/"; + const params = new URLSearchParams({ + s: keyword, + orderby: orderBy, + }); + + if (jobTypes && jobTypes.length > 0) { + params.append("job_type", jobTypes.join(",")); + } + + return `${baseUrl}?${params.toString()}`; +} + +/** + * SkipTheDrive parsing strategy function + */ +async function skipthedriveStrategy(coreParser, options = {}) { + const { + keywords = ["software engineer", "developer", "programmer"], + locationFilter = null, + maxPages = 5, + jobTypes = [], + } = options; + + const results = []; + const rejectedResults = []; + const seenJobs = new Set(); + + try { + // Create main page + const page = await coreParser.createPage("skipthedrive-main"); + + logger.info("šŸš€ Starting SkipTheDrive parser..."); + logger.info(`šŸ” Keywords: ${keywords.join(", ")}`); + logger.info(`šŸ“ Location Filter: ${locationFilter || "None"}`); + logger.info(`šŸ“„ Max Pages: ${maxPages}`); + + // Search for each keyword + for (const keyword of keywords) { + logger.info(`\nšŸ” Searching for: ${keyword}`); + + const searchUrl = buildSearchUrl(keyword, "date", jobTypes); + + try { + // Navigate to search results + await coreParser.navigateTo(searchUrl, { + pageId: "skipthedrive-main", + retries: 2, + timeout: 30000, + }); + + // Wait for job listings to load + const hasResults = await coreParser + .waitForSelector( + "#loops-wrapper", + { + timeout: 5000, + }, + "skipthedrive-main" + ) + .catch(() => { + logger.warning(`No results found for keyword: ${keyword}`); + return false; + }); + + if (!hasResults) { + continue; + } + + // Process multiple pages + let currentPage = 1; + let hasNextPage = true; + + while (hasNextPage && currentPage <= maxPages) { + logger.info(`šŸ“„ Processing page ${currentPage} for "${keyword}"`); + + // Extract jobs from current page + const pageJobs = await extractJobsFromPage( + page, + keyword, + locationFilter + ); + + for (const job of pageJobs) { + // Skip duplicates + if (seenJobs.has(job.jobId)) continue; + seenJobs.add(job.jobId); + + // Validate location if filtering enabled + if (locationFilter) { + const locationValid = validateLocationAgainstFilters( + job.location, + locationFilter + ); + + if (!locationValid) { + rejectedResults.push({ + ...job, + rejectionReason: "Location filter mismatch", + }); + continue; + } + } + + results.push(job); + } + + // Check for next page + hasNextPage = await hasNextPageAvailable(page); + if (hasNextPage && currentPage < maxPages) { + await navigateToNextPage(page, currentPage + 1); + currentPage++; + + // Wait for new page to load + await page.waitForTimeout(2000); + } else { + hasNextPage = false; + } + } + } catch (error) { + logger.error(`Error processing keyword "${keyword}": ${error.message}`); + } + } + + logger.info( + `šŸŽÆ SkipTheDrive parsing completed: ${results.length} jobs found, ${rejectedResults.length} rejected` + ); + + return { + results, + rejectedResults, + summary: { + totalJobs: results.length, + totalRejected: rejectedResults.length, + keywords: keywords.join(", "), + locationFilter, + source: "skipthedrive", + }, + }; + } catch (error) { + logger.error(`āŒ SkipTheDrive parsing failed: ${error.message}`); + throw error; + } +} + +/** + * Extract jobs from current page + */ +async function extractJobsFromPage(page, keyword, locationFilter) { + const jobs = []; + + try { + // Get all job article elements + const jobElements = await page.$$("article.job_listing"); + + for (const jobElement of jobElements) { + try { + const job = await extractJobData(jobElement, keyword); + if (job) { + jobs.push(job); + } + } catch (error) { + logger.warning(`Failed to extract job data: ${error.message}`); + } + } + } catch (error) { + logger.error(`Failed to extract jobs from page: ${error.message}`); + } + + return jobs; +} + +/** + * Extract data from individual job element + */ +async function extractJobData(jobElement, keyword) { + try { + // Extract job ID + const articleId = (await jobElement.getAttribute("id")) || ""; + const jobId = articleId ? articleId.replace("post-", "") : ""; + + // Extract title + const titleElement = await jobElement.$(".job_listing-title a"); + const title = titleElement + ? cleanText(await titleElement.textContent()) + : ""; + const jobUrl = titleElement ? await titleElement.getAttribute("href") : ""; + + // Extract company + const companyElement = await jobElement.$(".company"); + const company = companyElement + ? cleanText(await companyElement.textContent()) + : ""; + + // Extract location + const locationElement = await jobElement.$(".location"); + const location = locationElement + ? cleanText(await locationElement.textContent()) + : ""; + + // Extract date posted + const dateElement = await jobElement.$(".job-date"); + const dateText = dateElement + ? cleanText(await dateElement.textContent()) + : ""; + + // Extract description + const descElement = await jobElement.$(".job_listing-description"); + const description = descElement + ? cleanText(await descElement.textContent()) + : ""; + + // Check if featured + const featuredElement = await jobElement.$(".featured"); + const isFeatured = featuredElement !== null; + + // Parse date + let datePosted = null; + let daysAgo = null; + + if (dateText) { + const match = dateText.match(/(\d+)\s+days?\s+ago/); + if (match) { + daysAgo = parseInt(match[1]); + const date = new Date(); + date.setDate(date.getDate() - daysAgo); + datePosted = date.toISOString().split("T")[0]; + } + } + + return { + jobId, + title, + company, + location, + jobUrl, + datePosted, + dateText, + daysAgo, + description, + isFeatured, + keyword, + extractedAt: new Date().toISOString(), + source: "skipthedrive", + }; + } catch (error) { + logger.warning(`Error extracting job data: ${error.message}`); + return null; + } +} + +/** + * Check if next page is available + */ +async function hasNextPageAvailable(page) { + try { + const nextButton = await page.$(".next-page"); + return nextButton !== null; + } catch { + return false; + } +} + +/** + * Navigate to next page + */ +async function navigateToNextPage(page, pageNumber) { + try { + const nextButton = await page.$(".next-page"); + if (nextButton) { + await nextButton.click(); + } + } catch (error) { + logger.warning( + `Failed to navigate to page ${pageNumber}: ${error.message}` + ); + } +} + +module.exports = { + skipthedriveStrategy, + buildSearchUrl, + extractJobsFromPage, + extractJobData, +}; diff --git a/linkedin-parser/demo.js b/linkedin-parser/demo.js index 4dd36e0..1ae87a1 100644 --- a/linkedin-parser/demo.js +++ b/linkedin-parser/demo.js @@ -1,412 +1,412 @@ -/** - * LinkedIn Parser Demo - * - * Demonstrates the LinkedIn Parser's capabilities for scraping LinkedIn content - * with keyword-based searching, location filtering, and AI analysis. - * - * This demo uses simulated data for safety and demonstration purposes. - */ - -const { logger } = require("../ai-analyzer"); -const fs = require("fs"); -const path = require("path"); - -// Terminal colors for demo output -const colors = { - reset: "\x1b[0m", - bright: "\x1b[1m", - cyan: "\x1b[36m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - magenta: "\x1b[35m", - red: "\x1b[31m", -}; - -const demo = { - title: (text) => - console.log(`\n${colors.bright}${colors.cyan}${text}${colors.reset}`), - section: (text) => - console.log(`\n${colors.bright}${colors.magenta}${text}${colors.reset}`), - success: (text) => console.log(`${colors.green}āœ… ${text}${colors.reset}`), - info: (text) => console.log(`${colors.blue}ā„¹ļø ${text}${colors.reset}`), - warning: (text) => console.log(`${colors.yellow}āš ļø ${text}${colors.reset}`), - error: (text) => console.log(`${colors.red}āŒ ${text}${colors.reset}`), - code: (text) => console.log(`${colors.cyan}${text}${colors.reset}`), -}; - -// Mock data for demonstration -const mockPosts = [ - { - id: "post_1", - content: - "Just got laid off from my software engineering role at TechCorp. Looking for new opportunities in Toronto. This is really tough but I'm staying positive!", - original_content: - "Just got #laidoff from my software engineering role at TechCorp! Looking for new opportunities in #Toronto. This is really tough but I'm staying positive! šŸš€", - author: { - name: "John Doe", - title: "Software Engineer", - company: "TechCorp", - location: "Toronto, Ontario, Canada", - profile_url: "https://linkedin.com/in/johndoe", - }, - engagement: { likes: 45, comments: 12, shares: 3 }, - metadata: { - post_date: "2024-01-10T14:30:00Z", - scraped_at: "2024-01-15T10:30:00Z", - search_keyword: "layoff", - location_validated: true, - }, - }, - { - id: "post_2", - content: - "Our company is downsizing and I'm affected. This is really tough news but I'm grateful for the time I had here.", - original_content: - "Our company is #downsizing and I'm affected. This is really tough news but I'm grateful for the time I had here. #RIF #layoff", - author: { - name: "Jane Smith", - title: "Product Manager", - company: "StartupXYZ", - location: "Vancouver, British Columbia, Canada", - profile_url: "https://linkedin.com/in/janesmith", - }, - engagement: { likes: 23, comments: 8, shares: 1 }, - metadata: { - post_date: "2024-01-09T16:45:00Z", - scraped_at: "2024-01-15T10:30:00Z", - search_keyword: "downsizing", - location_validated: true, - }, - }, - { - id: "post_3", - content: - "Open to work! Looking for new opportunities in software development. I have 5 years of experience in React, Node.js, and cloud technologies.", - original_content: - "Open to work! Looking for new opportunities in software development. I have 5 years of experience in #React, #NodeJS, and #cloud technologies. #opentowork #jobsearch", - author: { - name: "Bob Wilson", - title: "Full Stack Developer", - company: "Freelance", - location: "Calgary, Alberta, Canada", - profile_url: "https://linkedin.com/in/bobwilson", - }, - engagement: { likes: 67, comments: 15, shares: 8 }, - metadata: { - post_date: "2024-01-08T11:20:00Z", - scraped_at: "2024-01-15T10:30:00Z", - search_keyword: "open to work", - location_validated: true, - }, - }, -]; - -async function runDemo() { - demo.title("=== LinkedIn Parser Demo ==="); - demo.info( - "This demo showcases the LinkedIn Parser's capabilities for scraping LinkedIn content." - ); - demo.info("All data shown is simulated for demonstration purposes."); - demo.info("Press Enter to continue through each section...\n"); - - await waitForEnter(); - - // 1. Configuration Demo - await demonstrateConfiguration(); - - // 2. Keyword Loading Demo - await demonstrateKeywordLoading(); - - // 3. Search Process Demo - await demonstrateSearchProcess(); - - // 4. Location Filtering Demo - await demonstrateLocationFiltering(); - - // 5. AI Analysis Demo - await demonstrateAIAnalysis(); - - // 6. Output Generation Demo - await demonstrateOutputGeneration(); - - demo.title("=== Demo Complete ==="); - demo.success("LinkedIn Parser demo completed successfully!"); - demo.info("Check the README.md for detailed usage instructions."); -} - -async function demonstrateConfiguration() { - demo.section("1. Configuration Setup"); - demo.info( - "The LinkedIn Parser uses environment variables and command-line options for configuration." - ); - - demo.code("// Environment Variables (.env file)"); - demo.info("LINKEDIN_USERNAME=your_email@example.com"); - demo.info("LINKEDIN_PASSWORD=your_password"); - demo.info("CITY=Toronto"); - demo.info("DATE_POSTED=past-week"); - demo.info("SORT_BY=date_posted"); - demo.info("WHEELS=5"); - demo.info("LOCATION_FILTER=Ontario,Manitoba"); - demo.info("ENABLE_LOCATION_CHECK=true"); - demo.info("ENABLE_LOCAL_AI=true"); - demo.info('AI_CONTEXT="job layoffs and workforce reduction"'); - demo.info("OLLAMA_MODEL=mistral"); - - demo.code("// Command Line Options"); - demo.info('node index.js --keyword="layoff,downsizing" --city="Vancouver"'); - demo.info("node index.js --no-location --no-ai"); - demo.info("node index.js --output=results/my-results.json"); - demo.info("node index.js --ai-after"); - - await waitForEnter(); -} - -async function demonstrateKeywordLoading() { - demo.section("2. Keyword Loading"); - demo.info( - "Keywords can be loaded from CSV files or specified via command line." - ); - - // Simulate loading keywords from CSV - demo.code("// Loading keywords from CSV file"); - logger.step("Loading keywords from keywords/linkedin-keywords.csv"); - - const keywords = [ - "layoff", - "downsizing", - "reduction in force", - "RIF", - "termination", - "job loss", - "workforce reduction", - "open to work", - "actively seeking", - "job search", - ]; - - demo.success(`Loaded ${keywords.length} keywords from CSV file`); - demo.info("Keywords: " + keywords.slice(0, 5).join(", ") + "..."); - - demo.code("// Command line keyword override"); - demo.info('node index.js --keyword="layoff,downsizing"'); - demo.info('node index.js --add-keyword="hiring freeze"'); - - await waitForEnter(); -} - -async function demonstrateSearchProcess() { - demo.section("3. Search Process Simulation"); - demo.info( - "The parser performs automated LinkedIn searches for each keyword." - ); - - const keywords = ["layoff", "downsizing", "open to work"]; - - for (const keyword of keywords) { - demo.code(`// Searching for keyword: "${keyword}"`); - logger.search(`Searching for "${keyword}" in Toronto`); - - // Simulate search process - await simulateSearch(); - - const foundCount = Math.floor(Math.random() * 50) + 10; - const acceptedCount = Math.floor(foundCount * 0.3); - - logger.info(`Found ${foundCount} posts, checking profiles for location...`); - logger.success(`Accepted ${acceptedCount} posts after location validation`); - - console.log(); - } - - await waitForEnter(); -} - -async function demonstrateLocationFiltering() { - demo.section("4. Location Filtering"); - demo.info( - "Posts are filtered based on author location using geographic validation." - ); - - demo.code("// Location filter configuration"); - demo.info("LOCATION_FILTER=Ontario,Manitoba"); - demo.info("ENABLE_LOCATION_CHECK=true"); - - demo.code("// Location validation examples"); - const testLocations = [ - { location: "Toronto, Ontario, Canada", valid: true }, - { location: "Vancouver, British Columbia, Canada", valid: false }, - { location: "Calgary, Alberta, Canada", valid: false }, - { location: "Winnipeg, Manitoba, Canada", valid: true }, - { location: "New York, NY, USA", valid: false }, - ]; - - testLocations.forEach(({ location, valid }) => { - logger.location(`Checking location: ${location}`); - if (valid) { - logger.success(`āœ… Location valid - post accepted`); - } else { - logger.warning(`āŒ Location invalid - post rejected`); - } - }); - - await waitForEnter(); -} - -async function demonstrateAIAnalysis() { - demo.section("5. AI Analysis"); - demo.info( - "Posts can be analyzed using local Ollama or OpenAI for relevance scoring." - ); - - demo.code("// AI analysis configuration"); - demo.info("ENABLE_LOCAL_AI=true"); - demo.info('AI_CONTEXT="job layoffs and workforce reduction"'); - demo.info("OLLAMA_MODEL=mistral"); - - demo.code("// Analyzing posts with AI"); - logger.ai("Starting AI analysis of accepted posts..."); - - for (let i = 0; i < mockPosts.length; i++) { - const post = mockPosts[i]; - logger.info(`Analyzing post ${i + 1}: ${post.content.substring(0, 50)}...`); - - // Simulate AI analysis - await simulateProcessing(); - - const relevanceScore = 0.7 + Math.random() * 0.3; - const confidence = 0.8 + Math.random() * 0.2; - - logger.success( - `Relevance: ${relevanceScore.toFixed( - 2 - )}, Confidence: ${confidence.toFixed(2)}` - ); - - // Add AI analysis to post - post.ai_analysis = { - relevance_score: relevanceScore, - confidence: confidence, - context_match: relevanceScore > 0.7, - analysis_text: `This post discusses ${post.metadata.search_keyword} and is relevant to the search context.`, - }; - } - - await waitForEnter(); -} - -async function demonstrateOutputGeneration() { - demo.section("6. Output Generation"); - demo.info("Results are saved to JSON files with comprehensive metadata."); - - demo.code("// Generating output file"); - logger.file("Saving results to JSON file..."); - - const outputData = { - metadata: { - timestamp: new Date().toISOString(), - keywords: ["layoff", "downsizing", "open to work"], - city: "Toronto", - date_posted: "past-week", - sort_by: "date_posted", - total_posts_found: 150, - accepted_posts: mockPosts.length, - rejected_posts: 147, - processing_time_seconds: 180, - }, - posts: mockPosts, - }; - - // Save to demo file - const outputPath = path.join(__dirname, "demo-results.json"); - fs.writeFileSync(outputPath, JSON.stringify(outputData, null, 2)); - - demo.success(`Results saved to: ${outputPath}`); - demo.info(`Total posts processed: ${outputData.metadata.total_posts_found}`); - demo.info(`Posts accepted: ${outputData.metadata.accepted_posts}`); - demo.info(`Posts rejected: ${outputData.metadata.rejected_posts}`); - - demo.code("// Output file structure"); - demo.info("šŸ“ demo-results.json"); - demo.info(" ā”œā”€ā”€ metadata"); - demo.info(" │ ā”œā”€ā”€ timestamp"); - demo.info(" │ ā”œā”€ā”€ keywords"); - demo.info(" │ ā”œā”€ā”€ city"); - demo.info(" │ ā”œā”€ā”€ total_posts_found"); - demo.info(" │ ā”œā”€ā”€ accepted_posts"); - demo.info(" │ └── processing_time_seconds"); - demo.info(" └── posts[]"); - demo.info(" ā”œā”€ā”€ id"); - demo.info(" ā”œā”€ā”€ content"); - demo.info(" ā”œā”€ā”€ author"); - demo.info(" ā”œā”€ā”€ engagement"); - demo.info(" ā”œā”€ā”€ ai_analysis"); - demo.info(" └── metadata"); - - await waitForEnter(); -} - -// Helper functions -function waitForEnter() { - return new Promise((resolve) => { - const readline = require("readline"); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rl.question("\nPress Enter to continue...", () => { - rl.close(); - resolve(); - }); - }); -} - -async function simulateSearch() { - return new Promise((resolve) => { - const steps = [ - "Launching browser", - "Logging in", - "Navigating to search", - "Loading results", - ]; - let i = 0; - const interval = setInterval(() => { - if (i < steps.length) { - logger.info(steps[i]); - i++; - } else { - clearInterval(interval); - resolve(); - } - }, 800); - }); -} - -async function simulateProcessing() { - return new Promise((resolve) => { - const dots = [".", "..", "..."]; - let i = 0; - const interval = setInterval(() => { - process.stdout.write(`\rProcessing${dots[i]}`); - i = (i + 1) % dots.length; - }, 500); - - setTimeout(() => { - clearInterval(interval); - process.stdout.write("\r"); - resolve(); - }, 1500); - }); -} - -// Run the demo if this file is executed directly -if (require.main === module) { - runDemo().catch((error) => { - demo.error(`Demo failed: ${error.message}`); - process.exit(1); - }); -} - -module.exports = { runDemo }; +/** + * LinkedIn Parser Demo + * + * Demonstrates the LinkedIn Parser's capabilities for scraping LinkedIn content + * with keyword-based searching, location filtering, and AI analysis. + * + * This demo uses simulated data for safety and demonstration purposes. + */ + +const { logger } = require("../ai-analyzer"); +const fs = require("fs"); +const path = require("path"); + +// Terminal colors for demo output +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + cyan: "\x1b[36m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + red: "\x1b[31m", +}; + +const demo = { + title: (text) => + console.log(`\n${colors.bright}${colors.cyan}${text}${colors.reset}`), + section: (text) => + console.log(`\n${colors.bright}${colors.magenta}${text}${colors.reset}`), + success: (text) => console.log(`${colors.green}āœ… ${text}${colors.reset}`), + info: (text) => console.log(`${colors.blue}ā„¹ļø ${text}${colors.reset}`), + warning: (text) => console.log(`${colors.yellow}āš ļø ${text}${colors.reset}`), + error: (text) => console.log(`${colors.red}āŒ ${text}${colors.reset}`), + code: (text) => console.log(`${colors.cyan}${text}${colors.reset}`), +}; + +// Mock data for demonstration +const mockPosts = [ + { + id: "post_1", + content: + "Just got laid off from my software engineering role at TechCorp. Looking for new opportunities in Toronto. This is really tough but I'm staying positive!", + original_content: + "Just got #laidoff from my software engineering role at TechCorp! Looking for new opportunities in #Toronto. This is really tough but I'm staying positive! šŸš€", + author: { + name: "John Doe", + title: "Software Engineer", + company: "TechCorp", + location: "Toronto, Ontario, Canada", + profile_url: "https://linkedin.com/in/johndoe", + }, + engagement: { likes: 45, comments: 12, shares: 3 }, + metadata: { + post_date: "2024-01-10T14:30:00Z", + scraped_at: "2024-01-15T10:30:00Z", + search_keyword: "layoff", + location_validated: true, + }, + }, + { + id: "post_2", + content: + "Our company is downsizing and I'm affected. This is really tough news but I'm grateful for the time I had here.", + original_content: + "Our company is #downsizing and I'm affected. This is really tough news but I'm grateful for the time I had here. #RIF #layoff", + author: { + name: "Jane Smith", + title: "Product Manager", + company: "StartupXYZ", + location: "Vancouver, British Columbia, Canada", + profile_url: "https://linkedin.com/in/janesmith", + }, + engagement: { likes: 23, comments: 8, shares: 1 }, + metadata: { + post_date: "2024-01-09T16:45:00Z", + scraped_at: "2024-01-15T10:30:00Z", + search_keyword: "downsizing", + location_validated: true, + }, + }, + { + id: "post_3", + content: + "Open to work! Looking for new opportunities in software development. I have 5 years of experience in React, Node.js, and cloud technologies.", + original_content: + "Open to work! Looking for new opportunities in software development. I have 5 years of experience in #React, #NodeJS, and #cloud technologies. #opentowork #jobsearch", + author: { + name: "Bob Wilson", + title: "Full Stack Developer", + company: "Freelance", + location: "Calgary, Alberta, Canada", + profile_url: "https://linkedin.com/in/bobwilson", + }, + engagement: { likes: 67, comments: 15, shares: 8 }, + metadata: { + post_date: "2024-01-08T11:20:00Z", + scraped_at: "2024-01-15T10:30:00Z", + search_keyword: "open to work", + location_validated: true, + }, + }, +]; + +async function runDemo() { + demo.title("=== LinkedIn Parser Demo ==="); + demo.info( + "This demo showcases the LinkedIn Parser's capabilities for scraping LinkedIn content." + ); + demo.info("All data shown is simulated for demonstration purposes."); + demo.info("Press Enter to continue through each section...\n"); + + await waitForEnter(); + + // 1. Configuration Demo + await demonstrateConfiguration(); + + // 2. Keyword Loading Demo + await demonstrateKeywordLoading(); + + // 3. Search Process Demo + await demonstrateSearchProcess(); + + // 4. Location Filtering Demo + await demonstrateLocationFiltering(); + + // 5. AI Analysis Demo + await demonstrateAIAnalysis(); + + // 6. Output Generation Demo + await demonstrateOutputGeneration(); + + demo.title("=== Demo Complete ==="); + demo.success("LinkedIn Parser demo completed successfully!"); + demo.info("Check the README.md for detailed usage instructions."); +} + +async function demonstrateConfiguration() { + demo.section("1. Configuration Setup"); + demo.info( + "The LinkedIn Parser uses environment variables and command-line options for configuration." + ); + + demo.code("// Environment Variables (.env file)"); + demo.info("LINKEDIN_USERNAME=your_email@example.com"); + demo.info("LINKEDIN_PASSWORD=your_password"); + demo.info("CITY=Toronto"); + demo.info("DATE_POSTED=past-week"); + demo.info("SORT_BY=date_posted"); + demo.info("WHEELS=5"); + demo.info("LOCATION_FILTER=Ontario,Manitoba"); + demo.info("ENABLE_LOCATION_CHECK=true"); + demo.info("ENABLE_LOCAL_AI=true"); + demo.info('AI_CONTEXT="job layoffs and workforce reduction"'); + demo.info("OLLAMA_MODEL=mistral"); + + demo.code("// Command Line Options"); + demo.info('node index.js --keyword="layoff,downsizing" --city="Vancouver"'); + demo.info("node index.js --no-location --no-ai"); + demo.info("node index.js --output=results/my-results.json"); + demo.info("node index.js --ai-after"); + + await waitForEnter(); +} + +async function demonstrateKeywordLoading() { + demo.section("2. Keyword Loading"); + demo.info( + "Keywords can be loaded from CSV files or specified via command line." + ); + + // Simulate loading keywords from CSV + demo.code("// Loading keywords from CSV file"); + logger.step("Loading keywords from keywords/linkedin-keywords.csv"); + + const keywords = [ + "layoff", + "downsizing", + "reduction in force", + "RIF", + "termination", + "job loss", + "workforce reduction", + "open to work", + "actively seeking", + "job search", + ]; + + demo.success(`Loaded ${keywords.length} keywords from CSV file`); + demo.info("Keywords: " + keywords.slice(0, 5).join(", ") + "..."); + + demo.code("// Command line keyword override"); + demo.info('node index.js --keyword="layoff,downsizing"'); + demo.info('node index.js --add-keyword="hiring freeze"'); + + await waitForEnter(); +} + +async function demonstrateSearchProcess() { + demo.section("3. Search Process Simulation"); + demo.info( + "The parser performs automated LinkedIn searches for each keyword." + ); + + const keywords = ["layoff", "downsizing", "open to work"]; + + for (const keyword of keywords) { + demo.code(`// Searching for keyword: "${keyword}"`); + logger.search(`Searching for "${keyword}" in Toronto`); + + // Simulate search process + await simulateSearch(); + + const foundCount = Math.floor(Math.random() * 50) + 10; + const acceptedCount = Math.floor(foundCount * 0.3); + + logger.info(`Found ${foundCount} posts, checking profiles for location...`); + logger.success(`Accepted ${acceptedCount} posts after location validation`); + + console.log(); + } + + await waitForEnter(); +} + +async function demonstrateLocationFiltering() { + demo.section("4. Location Filtering"); + demo.info( + "Posts are filtered based on author location using geographic validation." + ); + + demo.code("// Location filter configuration"); + demo.info("LOCATION_FILTER=Ontario,Manitoba"); + demo.info("ENABLE_LOCATION_CHECK=true"); + + demo.code("// Location validation examples"); + const testLocations = [ + { location: "Toronto, Ontario, Canada", valid: true }, + { location: "Vancouver, British Columbia, Canada", valid: false }, + { location: "Calgary, Alberta, Canada", valid: false }, + { location: "Winnipeg, Manitoba, Canada", valid: true }, + { location: "New York, NY, USA", valid: false }, + ]; + + testLocations.forEach(({ location, valid }) => { + logger.location(`Checking location: ${location}`); + if (valid) { + logger.success(`āœ… Location valid - post accepted`); + } else { + logger.warning(`āŒ Location invalid - post rejected`); + } + }); + + await waitForEnter(); +} + +async function demonstrateAIAnalysis() { + demo.section("5. AI Analysis"); + demo.info( + "Posts can be analyzed using local Ollama or OpenAI for relevance scoring." + ); + + demo.code("// AI analysis configuration"); + demo.info("ENABLE_LOCAL_AI=true"); + demo.info('AI_CONTEXT="job layoffs and workforce reduction"'); + demo.info("OLLAMA_MODEL=mistral"); + + demo.code("// Analyzing posts with AI"); + logger.ai("Starting AI analysis of accepted posts..."); + + for (let i = 0; i < mockPosts.length; i++) { + const post = mockPosts[i]; + logger.info(`Analyzing post ${i + 1}: ${post.content.substring(0, 50)}...`); + + // Simulate AI analysis + await simulateProcessing(); + + const relevanceScore = 0.7 + Math.random() * 0.3; + const confidence = 0.8 + Math.random() * 0.2; + + logger.success( + `Relevance: ${relevanceScore.toFixed( + 2 + )}, Confidence: ${confidence.toFixed(2)}` + ); + + // Add AI analysis to post + post.ai_analysis = { + relevance_score: relevanceScore, + confidence: confidence, + context_match: relevanceScore > 0.7, + analysis_text: `This post discusses ${post.metadata.search_keyword} and is relevant to the search context.`, + }; + } + + await waitForEnter(); +} + +async function demonstrateOutputGeneration() { + demo.section("6. Output Generation"); + demo.info("Results are saved to JSON files with comprehensive metadata."); + + demo.code("// Generating output file"); + logger.file("Saving results to JSON file..."); + + const outputData = { + metadata: { + timestamp: new Date().toISOString(), + keywords: ["layoff", "downsizing", "open to work"], + city: "Toronto", + date_posted: "past-week", + sort_by: "date_posted", + total_posts_found: 150, + accepted_posts: mockPosts.length, + rejected_posts: 147, + processing_time_seconds: 180, + }, + posts: mockPosts, + }; + + // Save to demo file + const outputPath = path.join(__dirname, "demo-results.json"); + fs.writeFileSync(outputPath, JSON.stringify(outputData, null, 2)); + + demo.success(`Results saved to: ${outputPath}`); + demo.info(`Total posts processed: ${outputData.metadata.total_posts_found}`); + demo.info(`Posts accepted: ${outputData.metadata.accepted_posts}`); + demo.info(`Posts rejected: ${outputData.metadata.rejected_posts}`); + + demo.code("// Output file structure"); + demo.info("šŸ“ demo-results.json"); + demo.info(" ā”œā”€ā”€ metadata"); + demo.info(" │ ā”œā”€ā”€ timestamp"); + demo.info(" │ ā”œā”€ā”€ keywords"); + demo.info(" │ ā”œā”€ā”€ city"); + demo.info(" │ ā”œā”€ā”€ total_posts_found"); + demo.info(" │ ā”œā”€ā”€ accepted_posts"); + demo.info(" │ └── processing_time_seconds"); + demo.info(" └── posts[]"); + demo.info(" ā”œā”€ā”€ id"); + demo.info(" ā”œā”€ā”€ content"); + demo.info(" ā”œā”€ā”€ author"); + demo.info(" ā”œā”€ā”€ engagement"); + demo.info(" ā”œā”€ā”€ ai_analysis"); + demo.info(" └── metadata"); + + await waitForEnter(); +} + +// Helper functions +function waitForEnter() { + return new Promise((resolve) => { + const readline = require("readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.question("\nPress Enter to continue...", () => { + rl.close(); + resolve(); + }); + }); +} + +async function simulateSearch() { + return new Promise((resolve) => { + const steps = [ + "Launching browser", + "Logging in", + "Navigating to search", + "Loading results", + ]; + let i = 0; + const interval = setInterval(() => { + if (i < steps.length) { + logger.info(steps[i]); + i++; + } else { + clearInterval(interval); + resolve(); + } + }, 800); + }); +} + +async function simulateProcessing() { + return new Promise((resolve) => { + const dots = [".", "..", "..."]; + let i = 0; + const interval = setInterval(() => { + process.stdout.write(`\rProcessing${dots[i]}`); + i = (i + 1) % dots.length; + }, 500); + + setTimeout(() => { + clearInterval(interval); + process.stdout.write("\r"); + resolve(); + }, 1500); + }); +} + +// Run the demo if this file is executed directly +if (require.main === module) { + runDemo().catch((error) => { + demo.error(`Demo failed: ${error.message}`); + process.exit(1); + }); +} + +module.exports = { runDemo }; diff --git a/linkedin-parser/keywords/linkedin-keywords.csv b/linkedin-parser/keywords/linkedin-keywords.csv index ab919db..53b5950 100644 --- a/linkedin-parser/keywords/linkedin-keywords.csv +++ b/linkedin-parser/keywords/linkedin-keywords.csv @@ -1,51 +1,51 @@ -keyword -acquisition -actively seeking -bankruptcy -business realignment -career transition -company closure -company reorganization -cost cutting -department closure -downsizing -furlough -headcount reduction -hiring -hiring freeze -involuntary separation -job cuts -job elimination -job loss -job opportunity -job search -layoff -looking for opportunities -mass layoff -merger -new position -new role -office closure -open to work -organizational change -outplacement -plant closure -position elimination -recruiting -reduction in force -redundancies -redundancy -restructuring -rightsizing -RIF -role elimination -separation -site closure -staff reduction -terminated -termination -voluntary separation -workforce adjustment -workforce optimization -workforce reduction -workforce transition +keyword +acquisition +actively seeking +bankruptcy +business realignment +career transition +company closure +company reorganization +cost cutting +department closure +downsizing +furlough +headcount reduction +hiring +hiring freeze +involuntary separation +job cuts +job elimination +job loss +job opportunity +job search +layoff +looking for opportunities +mass layoff +merger +new position +new role +office closure +open to work +organizational change +outplacement +plant closure +position elimination +recruiting +reduction in force +redundancies +redundancy +restructuring +rightsizing +RIF +role elimination +separation +site closure +staff reduction +terminated +termination +voluntary separation +workforce adjustment +workforce optimization +workforce reduction +workforce transition diff --git a/linkedin-parser/strategies/linkedin-strategy.js b/linkedin-parser/strategies/linkedin-strategy.js index 912eb87..a7705ae 100644 --- a/linkedin-parser/strategies/linkedin-strategy.js +++ b/linkedin-parser/strategies/linkedin-strategy.js @@ -1,230 +1,230 @@ -/** - * LinkedIn Parsing Strategy - * - * Uses core-parser for browser management and ai-analyzer for utilities - */ - -const { - logger, - cleanText, - containsAnyKeyword, - validateLocationAgainstFilters, - extractLocationFromProfile, -} = require("ai-analyzer"); - -/** - * LinkedIn parsing strategy function - */ -async function linkedinStrategy(coreParser, options = {}) { - const { - keywords = ["layoff", "downsizing", "job cuts"], - locationFilter = null, - maxResults = 50, - credentials = {}, - } = options; - - const results = []; - const rejectedResults = []; - const seenPosts = new Set(); - const seenProfiles = new Set(); - - try { - // Create main page - const page = await coreParser.createPage("linkedin-main"); - - // Authenticate to LinkedIn - logger.info("šŸ” Authenticating to LinkedIn..."); - await coreParser.authenticate("linkedin", credentials, "linkedin-main"); - logger.info("āœ… LinkedIn authentication successful"); - - // Search for posts with each keyword - for (const keyword of keywords) { - logger.info(`šŸ” Searching LinkedIn for: "${keyword}"`); - - const searchUrl = `https://www.linkedin.com/search/results/content/?keywords=${encodeURIComponent( - keyword - )}&sortBy=date_posted`; - - await coreParser.navigateTo(searchUrl, { - pageId: "linkedin-main", - retries: 2, - }); - - // Wait for search results - const hasResults = await coreParser.navigationManager.navigateAndWaitFor( - searchUrl, - ".search-results-container", - { pageId: "linkedin-main", timeout: 10000 } - ); - - if (!hasResults) { - logger.warning(`No search results found for keyword: ${keyword}`); - continue; - } - - // Extract posts from current page - const posts = await extractPostsFromPage(page, keyword); - - for (const post of posts) { - // Skip duplicates - if (seenPosts.has(post.postId)) continue; - seenPosts.add(post.postId); - - // Validate location if filtering enabled - if (locationFilter) { - const locationValid = validateLocationAgainstFilters( - post.location || post.profileLocation, - locationFilter - ); - - if (!locationValid) { - rejectedResults.push({ - ...post, - rejectionReason: "Location filter mismatch", - }); - continue; - } - } - - results.push(post); - - if (results.length >= maxResults) { - logger.info(`šŸ“Š Reached maximum results limit: ${maxResults}`); - break; - } - } - - if (results.length >= maxResults) break; - } - - logger.info( - `šŸŽÆ LinkedIn parsing completed: ${results.length} posts found, ${rejectedResults.length} rejected` - ); - - return { - results, - rejectedResults, - summary: { - totalPosts: results.length, - totalRejected: rejectedResults.length, - keywords: keywords.join(", "), - locationFilter, - }, - }; - } catch (error) { - logger.error(`āŒ LinkedIn parsing failed: ${error.message}`); - throw error; - } -} - -/** - * Extract posts from current search results page - */ -async function extractPostsFromPage(page, keyword) { - const posts = []; - - try { - // Get all post elements - const postElements = await page.$$(".feed-shared-update-v2"); - - for (const postElement of postElements) { - try { - const post = await extractPostData(postElement, keyword); - if (post) { - posts.push(post); - } - } catch (error) { - logger.warning(`Failed to extract post data: ${error.message}`); - } - } - } catch (error) { - logger.error(`Failed to extract posts from page: ${error.message}`); - } - - return posts; -} - -/** - * Extract data from individual post element - */ -async function extractPostData(postElement, keyword) { - try { - // Extract post ID - const postId = (await postElement.getAttribute("data-urn")) || ""; - - // Extract author info - const authorElement = await postElement.$(".feed-shared-actor__name"); - const authorName = authorElement - ? cleanText(await authorElement.textContent()) - : ""; - - const authorLinkElement = await postElement.$(".feed-shared-actor__name a"); - const authorUrl = authorLinkElement - ? await authorLinkElement.getAttribute("href") - : ""; - - // Extract post content - const contentElement = await postElement.$(".feed-shared-text"); - const content = contentElement - ? cleanText(await contentElement.textContent()) - : ""; - - // Extract timestamp - const timeElement = await postElement.$( - ".feed-shared-actor__sub-description time" - ); - const timestamp = timeElement - ? await timeElement.getAttribute("datetime") - : ""; - - // Extract engagement metrics - const likesElement = await postElement.$(".social-counts-reactions__count"); - const likesText = likesElement - ? cleanText(await likesElement.textContent()) - : "0"; - - const commentsElement = await postElement.$( - ".social-counts-comments__count" - ); - const commentsText = commentsElement - ? cleanText(await commentsElement.textContent()) - : "0"; - - // Check if post contains relevant keywords - const isRelevant = containsAnyKeyword(content, [keyword]); - - if (!isRelevant) { - return null; // Skip irrelevant posts - } - - return { - postId: cleanText(postId), - authorName, - authorUrl, - content, - timestamp, - keyword, - likes: extractNumber(likesText), - comments: extractNumber(commentsText), - extractedAt: new Date().toISOString(), - source: "linkedin", - }; - } catch (error) { - logger.warning(`Error extracting post data: ${error.message}`); - return null; - } -} - -/** - * Extract numbers from text (e.g., "15 likes" -> 15) - */ -function extractNumber(text) { - const match = text.match(/\d+/); - return match ? parseInt(match[0]) : 0; -} - -module.exports = { - linkedinStrategy, - extractPostsFromPage, - extractPostData, -}; +/** + * LinkedIn Parsing Strategy + * + * Uses core-parser for browser management and ai-analyzer for utilities + */ + +const { + logger, + cleanText, + containsAnyKeyword, + validateLocationAgainstFilters, + extractLocationFromProfile, +} = require("ai-analyzer"); + +/** + * LinkedIn parsing strategy function + */ +async function linkedinStrategy(coreParser, options = {}) { + const { + keywords = ["layoff", "downsizing", "job cuts"], + locationFilter = null, + maxResults = 50, + credentials = {}, + } = options; + + const results = []; + const rejectedResults = []; + const seenPosts = new Set(); + const seenProfiles = new Set(); + + try { + // Create main page + const page = await coreParser.createPage("linkedin-main"); + + // Authenticate to LinkedIn + logger.info("šŸ” Authenticating to LinkedIn..."); + await coreParser.authenticate("linkedin", credentials, "linkedin-main"); + logger.info("āœ… LinkedIn authentication successful"); + + // Search for posts with each keyword + for (const keyword of keywords) { + logger.info(`šŸ” Searching LinkedIn for: "${keyword}"`); + + const searchUrl = `https://www.linkedin.com/search/results/content/?keywords=${encodeURIComponent( + keyword + )}&sortBy=date_posted`; + + await coreParser.navigateTo(searchUrl, { + pageId: "linkedin-main", + retries: 2, + }); + + // Wait for search results + const hasResults = await coreParser.navigationManager.navigateAndWaitFor( + searchUrl, + ".search-results-container", + { pageId: "linkedin-main", timeout: 10000 } + ); + + if (!hasResults) { + logger.warning(`No search results found for keyword: ${keyword}`); + continue; + } + + // Extract posts from current page + const posts = await extractPostsFromPage(page, keyword); + + for (const post of posts) { + // Skip duplicates + if (seenPosts.has(post.postId)) continue; + seenPosts.add(post.postId); + + // Validate location if filtering enabled + if (locationFilter) { + const locationValid = validateLocationAgainstFilters( + post.location || post.profileLocation, + locationFilter + ); + + if (!locationValid) { + rejectedResults.push({ + ...post, + rejectionReason: "Location filter mismatch", + }); + continue; + } + } + + results.push(post); + + if (results.length >= maxResults) { + logger.info(`šŸ“Š Reached maximum results limit: ${maxResults}`); + break; + } + } + + if (results.length >= maxResults) break; + } + + logger.info( + `šŸŽÆ LinkedIn parsing completed: ${results.length} posts found, ${rejectedResults.length} rejected` + ); + + return { + results, + rejectedResults, + summary: { + totalPosts: results.length, + totalRejected: rejectedResults.length, + keywords: keywords.join(", "), + locationFilter, + }, + }; + } catch (error) { + logger.error(`āŒ LinkedIn parsing failed: ${error.message}`); + throw error; + } +} + +/** + * Extract posts from current search results page + */ +async function extractPostsFromPage(page, keyword) { + const posts = []; + + try { + // Get all post elements + const postElements = await page.$$(".feed-shared-update-v2"); + + for (const postElement of postElements) { + try { + const post = await extractPostData(postElement, keyword); + if (post) { + posts.push(post); + } + } catch (error) { + logger.warning(`Failed to extract post data: ${error.message}`); + } + } + } catch (error) { + logger.error(`Failed to extract posts from page: ${error.message}`); + } + + return posts; +} + +/** + * Extract data from individual post element + */ +async function extractPostData(postElement, keyword) { + try { + // Extract post ID + const postId = (await postElement.getAttribute("data-urn")) || ""; + + // Extract author info + const authorElement = await postElement.$(".feed-shared-actor__name"); + const authorName = authorElement + ? cleanText(await authorElement.textContent()) + : ""; + + const authorLinkElement = await postElement.$(".feed-shared-actor__name a"); + const authorUrl = authorLinkElement + ? await authorLinkElement.getAttribute("href") + : ""; + + // Extract post content + const contentElement = await postElement.$(".feed-shared-text"); + const content = contentElement + ? cleanText(await contentElement.textContent()) + : ""; + + // Extract timestamp + const timeElement = await postElement.$( + ".feed-shared-actor__sub-description time" + ); + const timestamp = timeElement + ? await timeElement.getAttribute("datetime") + : ""; + + // Extract engagement metrics + const likesElement = await postElement.$(".social-counts-reactions__count"); + const likesText = likesElement + ? cleanText(await likesElement.textContent()) + : "0"; + + const commentsElement = await postElement.$( + ".social-counts-comments__count" + ); + const commentsText = commentsElement + ? cleanText(await commentsElement.textContent()) + : "0"; + + // Check if post contains relevant keywords + const isRelevant = containsAnyKeyword(content, [keyword]); + + if (!isRelevant) { + return null; // Skip irrelevant posts + } + + return { + postId: cleanText(postId), + authorName, + authorUrl, + content, + timestamp, + keyword, + likes: extractNumber(likesText), + comments: extractNumber(commentsText), + extractedAt: new Date().toISOString(), + source: "linkedin", + }; + } catch (error) { + logger.warning(`Error extracting post data: ${error.message}`); + return null; + } +} + +/** + * Extract numbers from text (e.g., "15 likes" -> 15) + */ +function extractNumber(text) { + const match = text.match(/\d+/); + return match ? parseInt(match[0]) : 0; +} + +module.exports = { + linkedinStrategy, + extractPostsFromPage, + extractPostData, +}; diff --git a/sample-data.json b/sample-data.json index 3369cb9..50ee8ed 100644 --- a/sample-data.json +++ b/sample-data.json @@ -1,34 +1,34 @@ -{ - "results": [ - { - "text": "Just got laid off from my software engineering role. Looking for new opportunities in the Toronto area.", - "location": "Toronto, Ontario, Canada", - "keyword": "layoff", - "timestamp": "2024-01-15T10:30:00Z" - }, - { - "text": "Excited to share that I'm starting a new position as a Senior Developer at TechCorp!", - "location": "Vancouver, BC, Canada", - "keyword": "hiring", - "timestamp": "2024-01-15T11:00:00Z" - }, - { - "text": "Our company is going through a restructuring and unfortunately had to let go of 50 employees.", - "location": "Montreal, Quebec, Canada", - "keyword": "layoff", - "timestamp": "2024-01-15T11:30:00Z" - }, - { - "text": "Beautiful weather today! Perfect for a walk in the park.", - "location": "Calgary, Alberta, Canada", - "keyword": "weather", - "timestamp": "2024-01-15T12:00:00Z" - }, - { - "text": "We're hiring! Looking for talented developers to join our growing team.", - "location": "Ottawa, Ontario, Canada", - "keyword": "hiring", - "timestamp": "2024-01-15T12:30:00Z" - } - ] -} +{ + "results": [ + { + "text": "Just got laid off from my software engineering role. Looking for new opportunities in the Toronto area.", + "location": "Toronto, Ontario, Canada", + "keyword": "layoff", + "timestamp": "2024-01-15T10:30:00Z" + }, + { + "text": "Excited to share that I'm starting a new position as a Senior Developer at TechCorp!", + "location": "Vancouver, BC, Canada", + "keyword": "hiring", + "timestamp": "2024-01-15T11:00:00Z" + }, + { + "text": "Our company is going through a restructuring and unfortunately had to let go of 50 employees.", + "location": "Montreal, Quebec, Canada", + "keyword": "layoff", + "timestamp": "2024-01-15T11:30:00Z" + }, + { + "text": "Beautiful weather today! Perfect for a walk in the park.", + "location": "Calgary, Alberta, Canada", + "keyword": "weather", + "timestamp": "2024-01-15T12:00:00Z" + }, + { + "text": "We're hiring! Looking for talented developers to join our growing team.", + "location": "Ottawa, Ontario, Canada", + "keyword": "hiring", + "timestamp": "2024-01-15T12:30:00Z" + } + ] +}