diff --git a/scripts/dev.js b/scripts/dev.js index 052865f2..4841b4b8 100755 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -45,6 +45,7 @@ import path from 'path'; import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; +import readline from 'readline'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -108,10 +109,37 @@ function writeJSON(filepath, data) { fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8'); } -async function callClaude(prdContent, prdPath, numTasks) { +// Add a simple loading indicator function +function startLoadingIndicator(message) { + const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + let i = 0; + + process.stdout.write(`${message} `); + + return setInterval(() => { + readline.cursorTo(process.stdout, message.length + 1); + process.stdout.write(frames[i]); + i = (i + 1) % frames.length; + }, 80); +} + +function stopLoadingIndicator(interval) { + clearInterval(interval); + readline.cursorTo(process.stdout, 0); + readline.clearLine(process.stdout, 0); +} + +async function callClaude(prdContent, prdPath, numTasks, retryCount = 0) { + const MAX_RETRIES = 3; + const INITIAL_BACKOFF_MS = 1000; + log('info', `Starting Claude API call to process PRD from ${prdPath}...`); log('debug', `PRD content length: ${prdContent.length} characters`); + // Start loading indicator + const loadingMessage = `Waiting for Claude to generate tasks${retryCount > 0 ? ` (retry ${retryCount}/${MAX_RETRIES})` : ''}...`; + const loadingIndicator = startLoadingIndicator(loadingMessage); + const TASKS_JSON_TEMPLATE = ` { "meta": { @@ -154,44 +182,155 @@ async function callClaude(prdContent, prdPath, numTasks) { } log('debug', "System prompt:", systemPrompt); - log('info', "Sending request to Claude API..."); - - const response = await anthropic.messages.create({ - max_tokens: CONFIG.maxTokens, - model: CONFIG.model, - temperature: CONFIG.temperature, - messages: [ - { - role: "user", - content: prdContent - } - ], - system: systemPrompt - }); - log('info', "Received response from Claude API!"); - - // Extract the text content from the response - const textContent = response.content[0].text; - log('debug', `Response length: ${textContent.length} characters`); try { - // Check if the response is wrapped in a Markdown code block and extract the JSON - log('info', "Parsing response as JSON..."); - let jsonText = textContent; - const codeBlockMatch = textContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/); - if (codeBlockMatch) { - log('debug', "Detected JSON wrapped in Markdown code block, extracting..."); - jsonText = codeBlockMatch[1]; + // Calculate appropriate max tokens based on PRD size + let maxTokens = CONFIG.maxTokens; + // Rough estimate: 1 token ≈ 4 characters + const estimatedPrdTokens = Math.ceil(prdContent.length / 4); + // Ensure we have enough tokens for the response + if (estimatedPrdTokens > maxTokens / 2) { + // If PRD is large, increase max tokens if possible + const suggestedMaxTokens = Math.min(32000, estimatedPrdTokens * 2); + if (suggestedMaxTokens > maxTokens) { + log('info', `PRD is large (est. ${estimatedPrdTokens} tokens). Increasing max_tokens to ${suggestedMaxTokens}.`); + maxTokens = suggestedMaxTokens; + } } - // Try to parse the response as JSON - const parsedJson = JSON.parse(jsonText); - log('info', `Successfully parsed JSON with ${parsedJson.tasks?.length || 0} tasks`); - return parsedJson; + log('info', "Sending request to Claude API..."); + + const response = await anthropic.messages.create({ + max_tokens: maxTokens, + model: CONFIG.model, + temperature: CONFIG.temperature, + messages: [ + { + role: "user", + content: prdContent + } + ], + system: systemPrompt + }); + + // Stop loading indicator + stopLoadingIndicator(loadingIndicator); + log('info', "Received response from Claude API!"); + + // Extract the text content from the response + const textContent = response.content[0].text; + log('debug', `Response length: ${textContent.length} characters`); + + try { + // Check if the response is wrapped in a Markdown code block and extract the JSON + log('info', "Parsing response as JSON..."); + let jsonText = textContent; + const codeBlockMatch = textContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/); + if (codeBlockMatch) { + log('debug', "Detected JSON wrapped in Markdown code block, extracting..."); + jsonText = codeBlockMatch[1]; + } + + // Try to parse the response as JSON + const parsedJson = JSON.parse(jsonText); + + // Check if the response seems incomplete (e.g., missing closing brackets) + if (!parsedJson.tasks || parsedJson.tasks.length === 0) { + log('warn', "Parsed JSON has no tasks. Response may be incomplete."); + + // If we have a numTasks parameter and it's greater than 5, try again with fewer tasks + if (numTasks && numTasks > 5 && retryCount < MAX_RETRIES) { + const reducedTasks = Math.max(5, Math.floor(numTasks * 0.7)); // Reduce by 30%, minimum 5 + log('info', `Retrying with reduced task count: ${reducedTasks} (was ${numTasks})`); + return callClaude(prdContent, prdPath, reducedTasks, retryCount + 1); + } + } + + log('info', `Successfully parsed JSON with ${parsedJson.tasks?.length || 0} tasks`); + return parsedJson; + } catch (error) { + log('error', "Failed to parse Claude's response as JSON:", error); + log('debug', "Raw response:", textContent); + + // Check if we should retry with different parameters + if (retryCount < MAX_RETRIES) { + // If we have a numTasks parameter, try again with fewer tasks + if (numTasks && numTasks > 3) { + const reducedTasks = Math.max(3, Math.floor(numTasks * 0.6)); // Reduce by 40%, minimum 3 + log('info', `Retrying with reduced task count: ${reducedTasks} (was ${numTasks})`); + return callClaude(prdContent, prdPath, reducedTasks, retryCount + 1); + } else { + // Otherwise, just retry with the same parameters + log('info', `Retrying Claude API call (attempt ${retryCount + 1}/${MAX_RETRIES})...`); + return callClaude(prdContent, prdPath, numTasks, retryCount + 1); + } + } + + throw new Error("Failed to parse Claude's response as JSON after multiple attempts. See console for details."); + } } catch (error) { - log('error', "Failed to parse Claude's response as JSON:", error); - log('debug', "Raw response:", textContent); - throw new Error("Failed to parse Claude's response as JSON. See console for details."); + // Stop loading indicator + stopLoadingIndicator(loadingIndicator); + + log('error', "Error calling Claude API:", error); + + // Implement exponential backoff for retries + if (retryCount < MAX_RETRIES) { + const backoffTime = INITIAL_BACKOFF_MS * Math.pow(2, retryCount); + log('info', `Retrying in ${backoffTime/1000} seconds (attempt ${retryCount + 1}/${MAX_RETRIES})...`); + + await new Promise(resolve => setTimeout(resolve, backoffTime)); + + // If we have a numTasks parameter and it's greater than 3, try again with fewer tasks + if (numTasks && numTasks > 3) { + const reducedTasks = Math.max(3, Math.floor(numTasks * 0.7)); // Reduce by 30%, minimum 3 + log('info', `Retrying with reduced task count: ${reducedTasks} (was ${numTasks})`); + return callClaude(prdContent, prdPath, reducedTasks, retryCount + 1); + } else { + // Otherwise, just retry with the same parameters + return callClaude(prdContent, prdPath, numTasks, retryCount + 1); + } + } + + // If we've exhausted all retries, ask the user what to do + console.log("\nClaude API call failed after multiple attempts."); + console.log("Options:"); + console.log("1. Retry with the same parameters"); + console.log("2. Retry with fewer tasks (if applicable)"); + console.log("3. Abort"); + + const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve, reject) => { + readline.question('Enter your choice (1-3): ', async (choice) => { + readline.close(); + + switch (choice) { + case '1': + console.log("Retrying with the same parameters..."); + resolve(await callClaude(prdContent, prdPath, numTasks, 0)); // Reset retry count + break; + case '2': + if (numTasks && numTasks > 2) { + const reducedTasks = Math.max(2, Math.floor(numTasks * 0.5)); // Reduce by 50%, minimum 2 + console.log(`Retrying with reduced task count: ${reducedTasks} (was ${numTasks})...`); + resolve(await callClaude(prdContent, prdPath, reducedTasks, 0)); // Reset retry count + } else { + console.log("Cannot reduce task count further. Retrying with the same parameters..."); + resolve(await callClaude(prdContent, prdPath, numTasks, 0)); // Reset retry count + } + break; + case '3': + default: + console.log("Aborting..."); + reject(new Error("User aborted after multiple failed attempts")); + break; + } + }); + }); } } @@ -210,32 +349,38 @@ async function parsePRD(prdPath, tasksPath, numTasks) { // call claude to generate the tasks.json log('info', "Calling Claude to generate tasks from PRD..."); - const claudeResponse = await callClaude(prdContent, prdPath, numTasks); - let tasks = claudeResponse.tasks || []; - log('info', `Claude generated ${tasks.length} tasks from the PRD`); + + try { + const claudeResponse = await callClaude(prdContent, prdPath, numTasks); + let tasks = claudeResponse.tasks || []; + log('info', `Claude generated ${tasks.length} tasks from the PRD`); - // Limit the number of tasks if specified - if (numTasks && numTasks > 0 && numTasks < tasks.length) { - log('info', `Limiting to the first ${numTasks} tasks as specified`); - tasks = tasks.slice(0, numTasks); + // Limit the number of tasks if specified + if (numTasks && numTasks > 0 && numTasks < tasks.length) { + log('info', `Limiting to the first ${numTasks} tasks as specified`); + tasks = tasks.slice(0, numTasks); + } + + log('info', "Creating tasks.json data structure..."); + const data = { + meta: { + projectName: CONFIG.projectName, + version: CONFIG.projectVersion, + source: prdPath, + description: "Tasks generated from PRD", + totalTasksGenerated: claudeResponse.tasks?.length || 0, + tasksIncluded: tasks.length + }, + tasks + }; + + log('info', `Writing ${tasks.length} tasks to ${tasksPath}...`); + writeJSON(tasksPath, data); + log('info', `Parsed PRD from '${prdPath}' -> wrote ${tasks.length} tasks to '${tasksPath}'.`); + } catch (error) { + log('error', "Failed to generate tasks:", error.message); + process.exit(1); } - - log('info', "Creating tasks.json data structure..."); - const data = { - meta: { - projectName: CONFIG.projectName, - version: CONFIG.projectVersion, - source: prdPath, - description: "Tasks generated from PRD", - totalTasksGenerated: claudeResponse.tasks?.length || 0, - tasksIncluded: tasks.length - }, - tasks - }; - - log('info', `Writing ${tasks.length} tasks to ${tasksPath}...`); - writeJSON(tasksPath, data); - log('info', `Parsed PRD from '${prdPath}' -> wrote ${tasks.length} tasks to '${tasksPath}'.`); } // diff --git a/scripts/test-claude-errors.js b/scripts/test-claude-errors.js new file mode 100755 index 00000000..f224eb44 --- /dev/null +++ b/scripts/test-claude-errors.js @@ -0,0 +1,212 @@ +#!/usr/bin/env node + +/** + * test-claude-errors.js + * + * A test script to verify the error handling and retry logic in the callClaude function. + * This script creates a modified version of dev.js that simulates different error scenarios. + */ + +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import { execSync, spawn } from 'child_process'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Load environment variables from .env file +dotenv.config(); + +// Create a simple PRD for testing +const createTestPRD = () => { + return `# Test PRD for Error Handling + +## Overview +This is a simple test PRD to verify the error handling in the callClaude function. + +## Requirements +1. Create a simple web application +2. Implement user authentication +3. Add a dashboard for users +`; +}; + +// Create a modified version of dev.js that simulates errors +function createErrorSimulationScript(errorType, failureCount = 2) { + // Read the original dev.js file + const devJsPath = path.join(__dirname, 'dev.js'); + const devJsContent = fs.readFileSync(devJsPath, 'utf8'); + + // Create a modified version that simulates errors + let modifiedContent = devJsContent; + + // Find the anthropic.messages.create call and replace it with our mock + const anthropicCallRegex = /const response = await anthropic\.messages\.create\(/; + + let mockCode = ''; + + switch (errorType) { + case 'network': + mockCode = ` + // Mock for network error simulation + let currentAttempt = 0; + const failureCount = ${failureCount}; + + // Simulate network error for the first few attempts + currentAttempt++; + console.log(\`[Mock] API call attempt \${currentAttempt}\`); + + if (currentAttempt <= failureCount) { + console.log(\`[Mock] Simulating network error (attempt \${currentAttempt}/\${failureCount})\`); + throw new Error('Network error: Connection refused'); + } + + const response = await anthropic.messages.create(`; + break; + + case 'timeout': + mockCode = ` + // Mock for timeout error simulation + let currentAttempt = 0; + const failureCount = ${failureCount}; + + // Simulate timeout error for the first few attempts + currentAttempt++; + console.log(\`[Mock] API call attempt \${currentAttempt}\`); + + if (currentAttempt <= failureCount) { + console.log(\`[Mock] Simulating timeout error (attempt \${currentAttempt}/\${failureCount})\`); + throw new Error('Request timed out after 60000ms'); + } + + const response = await anthropic.messages.create(`; + break; + + case 'invalid-json': + mockCode = ` + // Mock for invalid JSON response + let currentAttempt = 0; + const failureCount = ${failureCount}; + + // Simulate invalid JSON for the first few attempts + currentAttempt++; + console.log(\`[Mock] API call attempt \${currentAttempt}\`); + + if (currentAttempt <= failureCount) { + console.log(\`[Mock] Simulating invalid JSON response (attempt \${currentAttempt}/\${failureCount})\`); + return { + content: [ + { + text: \`\`\`json\\n{"meta": {"projectName": "Test Project"}, "tasks": [{"id": 1, "title": "Task 1"\` + } + ] + }; + } + + const response = await anthropic.messages.create(`; + break; + + case 'empty-tasks': + mockCode = ` + // Mock for empty tasks array + let currentAttempt = 0; + const failureCount = ${failureCount}; + + // Simulate empty tasks array for the first few attempts + currentAttempt++; + console.log(\`[Mock] API call attempt \${currentAttempt}\`); + + if (currentAttempt <= failureCount) { + console.log(\`[Mock] Simulating empty tasks array (attempt \${currentAttempt}/\${failureCount})\`); + return { + content: [ + { + text: \`\`\`json\\n{"meta": {"projectName": "Test Project"}, "tasks": []}\\n\`\`\` + } + ] + }; + } + + const response = await anthropic.messages.create(`; + break; + + default: + // No modification + mockCode = `const response = await anthropic.messages.create(`; + } + + // Replace the anthropic call with our mock + modifiedContent = modifiedContent.replace(anthropicCallRegex, mockCode); + + // Write the modified script to a temporary file + const tempScriptPath = path.join(__dirname, `temp-dev-${errorType}.js`); + fs.writeFileSync(tempScriptPath, modifiedContent, 'utf8'); + + return tempScriptPath; +} + +// Function to run a test with a specific error type +async function runErrorTest(errorType, numTasks = 5, failureCount = 2) { + console.log(`\n=== Test: ${errorType.toUpperCase()} Error Simulation ===`); + + // Create a test PRD + const testPRD = createTestPRD(); + const testPRDPath = path.join(__dirname, `test-prd-${errorType}.txt`); + fs.writeFileSync(testPRDPath, testPRD, 'utf8'); + + // Create a modified dev.js that simulates the specified error + const tempScriptPath = createErrorSimulationScript(errorType, failureCount); + + console.log(`Created test PRD at ${testPRDPath}`); + console.log(`Created error simulation script at ${tempScriptPath}`); + console.log(`Running with error type: ${errorType}, failure count: ${failureCount}, tasks: ${numTasks}`); + + try { + // Run the modified script + execSync(`node ${tempScriptPath} parse-prd --input=${testPRDPath} --tasks=${numTasks}`, { + stdio: 'inherit' + }); + console.log(`${errorType} error test completed successfully`); + } catch (error) { + console.error(`${errorType} error test failed:`, error.message); + } finally { + // Clean up temporary files + if (fs.existsSync(tempScriptPath)) { + fs.unlinkSync(tempScriptPath); + } + if (fs.existsSync(testPRDPath)) { + fs.unlinkSync(testPRDPath); + } + } +} + +// Function to run all error tests +async function runAllErrorTests() { + console.log('Starting error handling tests for callClaude function...'); + + // Test 1: Network error with automatic retry + await runErrorTest('network', 5, 2); + + // Test 2: Timeout error with automatic retry + await runErrorTest('timeout', 5, 2); + + // Test 3: Invalid JSON response with task reduction + await runErrorTest('invalid-json', 10, 2); + + // Test 4: Empty tasks array with task reduction + await runErrorTest('empty-tasks', 15, 2); + + // Test 5: Exhausted retries (more failures than MAX_RETRIES) + await runErrorTest('network', 5, 4); + + console.log('\nAll error tests completed!'); +} + +// Run the tests +runAllErrorTests().catch(error => { + console.error('Error running tests:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/test-claude.js b/scripts/test-claude.js new file mode 100755 index 00000000..f3599ac4 --- /dev/null +++ b/scripts/test-claude.js @@ -0,0 +1,231 @@ +#!/usr/bin/env node + +/** + * test-claude.js + * + * A simple test script to verify the improvements to the callClaude function. + * This script tests different scenarios: + * 1. Normal operation with a small PRD + * 2. Testing with a large number of tasks (to potentially trigger task reduction) + * 3. Simulating a failure to test retry logic + */ + +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Load environment variables from .env file +dotenv.config(); + +// Create a simple PRD for testing +const createTestPRD = (size = 'small', taskComplexity = 'simple') => { + let content = `# Test PRD - ${size.toUpperCase()} SIZE, ${taskComplexity.toUpperCase()} COMPLEXITY\n\n`; + + // Add more content based on size + if (size === 'small') { + content += ` +## Overview +This is a small test PRD to verify the callClaude function improvements. + +## Requirements +1. Create a simple web application +2. Implement user authentication +3. Add a dashboard for users +4. Create an admin panel +5. Implement data visualization + +## Technical Stack +- Frontend: React +- Backend: Node.js +- Database: MongoDB +`; + } else if (size === 'medium') { + // Medium-sized PRD with more requirements + content += ` +## Overview +This is a medium-sized test PRD to verify the callClaude function improvements. + +## Requirements +1. Create a web application with multiple pages +2. Implement user authentication with OAuth +3. Add a dashboard for users with customizable widgets +4. Create an admin panel with user management +5. Implement data visualization with charts and graphs +6. Add real-time notifications +7. Implement a search feature +8. Add user profile management +9. Implement role-based access control +10. Add a reporting system +11. Implement file uploads and management +12. Add a commenting system +13. Implement a rating system +14. Add a recommendation engine +15. Implement a payment system + +## Technical Stack +- Frontend: React with TypeScript +- Backend: Node.js with Express +- Database: MongoDB with Mongoose +- Authentication: JWT and OAuth +- Deployment: Docker and Kubernetes +- CI/CD: GitHub Actions +- Monitoring: Prometheus and Grafana +`; + } else if (size === 'large') { + // Large PRD with many requirements + content += ` +## Overview +This is a large test PRD to verify the callClaude function improvements. + +## Requirements +`; + // Generate 30 requirements + for (let i = 1; i <= 30; i++) { + content += `${i}. Requirement ${i} - This is a detailed description of requirement ${i}.\n`; + } + + content += ` +## Technical Stack +- Frontend: React with TypeScript +- Backend: Node.js with Express +- Database: MongoDB with Mongoose +- Authentication: JWT and OAuth +- Deployment: Docker and Kubernetes +- CI/CD: GitHub Actions +- Monitoring: Prometheus and Grafana + +## User Stories +`; + // Generate 20 user stories + for (let i = 1; i <= 20; i++) { + content += `- As a user, I want to be able to ${i} so that I can achieve benefit ${i}.\n`; + } + + content += ` +## Non-Functional Requirements +- Performance: The system should respond within 200ms +- Scalability: The system should handle 10,000 concurrent users +- Availability: The system should have 99.9% uptime +- Security: The system should comply with OWASP top 10 +- Accessibility: The system should comply with WCAG 2.1 AA +`; + } + + // Add complexity if needed + if (taskComplexity === 'complex') { + content += ` +## Complex Requirements +- Implement a real-time collaboration system +- Add a machine learning-based recommendation engine +- Implement a distributed caching system +- Add a microservices architecture +- Implement a custom analytics engine +- Add support for multiple languages and locales +- Implement a custom search engine with advanced filtering +- Add a custom workflow engine +- Implement a custom reporting system +- Add a custom dashboard builder +`; + } + + return content; +}; + +// Function to run the tests +async function runTests() { + console.log('Starting tests for callClaude function improvements...'); + + try { + // Instead of importing the callClaude function directly, we'll use the dev.js script + // with our test PRDs by running it as a child process + + // Test 1: Small PRD, 5 tasks + console.log('\n=== Test 1: Small PRD, 5 tasks ==='); + const smallPRD = createTestPRD('small', 'simple'); + const smallPRDPath = path.join(__dirname, 'test-small-prd.txt'); + fs.writeFileSync(smallPRDPath, smallPRD, 'utf8'); + + console.log(`Created test PRD at ${smallPRDPath}`); + console.log('Running dev.js with small PRD...'); + + // Use the child_process module to run the dev.js script + const { execSync } = await import('child_process'); + + try { + const smallResult = execSync(`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${smallPRDPath} --tasks=5`, { + stdio: 'inherit' + }); + console.log('Small PRD test completed successfully'); + } catch (error) { + console.error('Small PRD test failed:', error.message); + } + + // Test 2: Medium PRD, 15 tasks + console.log('\n=== Test 2: Medium PRD, 15 tasks ==='); + const mediumPRD = createTestPRD('medium', 'simple'); + const mediumPRDPath = path.join(__dirname, 'test-medium-prd.txt'); + fs.writeFileSync(mediumPRDPath, mediumPRD, 'utf8'); + + console.log(`Created test PRD at ${mediumPRDPath}`); + console.log('Running dev.js with medium PRD...'); + + try { + const mediumResult = execSync(`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${mediumPRDPath} --tasks=15`, { + stdio: 'inherit' + }); + console.log('Medium PRD test completed successfully'); + } catch (error) { + console.error('Medium PRD test failed:', error.message); + } + + // Test 3: Large PRD, 25 tasks + console.log('\n=== Test 3: Large PRD, 25 tasks ==='); + const largePRD = createTestPRD('large', 'complex'); + const largePRDPath = path.join(__dirname, 'test-large-prd.txt'); + fs.writeFileSync(largePRDPath, largePRD, 'utf8'); + + console.log(`Created test PRD at ${largePRDPath}`); + console.log('Running dev.js with large PRD...'); + + try { + const largeResult = execSync(`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${largePRDPath} --tasks=25`, { + stdio: 'inherit' + }); + console.log('Large PRD test completed successfully'); + } catch (error) { + console.error('Large PRD test failed:', error.message); + } + + console.log('\nAll tests completed!'); + } catch (error) { + console.error('Test failed:', error); + } finally { + // Clean up test files + console.log('\nCleaning up test files...'); + const testFiles = [ + path.join(__dirname, 'test-small-prd.txt'), + path.join(__dirname, 'test-medium-prd.txt'), + path.join(__dirname, 'test-large-prd.txt') + ]; + + testFiles.forEach(file => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + console.log(`Deleted ${file}`); + } + }); + + console.log('Cleanup complete.'); + } +} + +// Run the tests +runTests().catch(error => { + console.error('Error running tests:', error); + process.exit(1); +}); \ No newline at end of file