ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
514 lines (441 loc) • 16.8 kB
JavaScript
/**
* Error Handler (JavaScript wrapper)
*
* This is a JavaScript wrapper for the TypeScript errorHandler module.
* It provides standardized error handling and recovery strategies.
*/
const fs = require('fs');
const path = require('path');
const { PathUtils, FileUtils } = require('./platformUtils');
// Error categories
const ErrorCategory = {
FILESYSTEM: 'filesystem',
NETWORK: 'network',
CONFIG: 'configuration',
VALIDATION: 'validation',
GENERATION: 'generation',
EXECUTION: 'execution',
SECURITY: 'security',
FRAMEWORK: 'framework',
PERMISSION: 'permission',
UNKNOWN: 'unknown'
};
// Fallback templates for various operations
const FALLBACK_TEMPLATES = {
'test': `
import { test, expect } from '@playwright/test';
/**
* Fallback test generated by ctrl.shift.left
*
* This is a basic test template created when full test generation failed.
* You can use this as a starting point to write your own tests.
*/
test.describe('Basic Component Test', () => {
test('should render without errors', async ({ page }) => {
// Navigate to your component or page
await page.goto('/');
// Wait for component to be visible
await page.waitForSelector('.your-component-class');
// Basic assertion
await expect(page.locator('.your-component-class')).toBeVisible();
});
});`,
'checklist': `{
"name": "QA Checklist",
"version": "1.0.0",
"timestamp": "${new Date().toISOString()}",
"items": [
{
"category": "Functionality",
"checks": [
{ "name": "Component renders without errors", "status": "pending" },
{ "name": "All interactive elements work as expected", "status": "pending" },
{ "name": "Error states are handled appropriately", "status": "pending" }
]
},
{
"category": "Security",
"checks": [
{ "name": "Input validation is implemented", "status": "pending" },
{ "name": "No sensitive data is exposed", "status": "pending" },
{ "name": "Authentication controls are enforced", "status": "pending" }
]
}
]
}`,
'security-report': `
# Security Analysis
*Generated by ctrl.shift.left (Fallback Template)*
## Summary
This is a fallback security report generated when the full security analysis could not be completed.
## Recommended Manual Checks
1. **Check for XSS vulnerabilities**
- Review any areas where user input is displayed in the UI
- Ensure proper encoding and sanitization is applied
2. **Check for authentication issues**
- Verify that authentication controls are properly implemented
- Ensure sensitive operations require authentication
3. **Check for data exposure**
- Review API responses for sensitive information
- Ensure proper authorization checks are in place
## Next Steps
1. Run a manual security review
2. Consider using dedicated security scanning tools
3. Fix any issues found and regenerate the security report
`
};
/**
* Error handler class for standardizing error handling across ctrl.shift.left
*/
class ErrorHandler {
/**
* Create a new error handler
* @param {Object} options Configuration options
* @param {string} options.logPath Path to error log file
* @param {boolean} options.verbose Whether to show verbose error output
* @param {boolean} options.autoRecover Whether to attempt automatic recovery
*/
constructor(options = {}) {
this.logPath = options.logPath || path.join(process.cwd(), '.ctrlshiftleft', 'logs', 'error.log');
this.verbose = options.verbose || false;
this.autoRecover = options.autoRecover || true;
// Ensure log directory exists
const logDir = path.dirname(this.logPath);
if (!fs.existsSync(logDir)) {
PathUtils.ensureDir(logDir);
}
}
/**
* Process and enhance an error with additional context
* @param {Error|string} error Original error
* @param {string} operation Operation that caused the error
* @param {Object} context Additional context information
* @returns {Object} Enhanced error object
*/
handleError(error, operation, context = {}) {
const errorMessage = error instanceof Error ? error.message : String(error);
const category = this.categorizeError(errorMessage, operation);
const code = this.generateErrorCode(category, operation);
const recovery = this.getRecoverySteps(category, operation, context);
const docLink = this.getDocumentationLink(category, code);
const enhancedError = {
message: errorMessage,
originalError: error instanceof Error ? error : new Error(error),
code,
category,
recovery,
docLink,
context
};
// Log the error
this.logError(enhancedError);
return enhancedError;
}
/**
* Display a user-friendly error message
* @param {Object} error Enhanced error object
*/
displayError(error) {
console.error('\n❌ Error:', error.message);
console.error(`Error code: ${error.code} (${error.category})`);
if (this.verbose && error.originalError?.stack) {
console.error('\nStack trace:');
console.error(error.originalError.stack);
}
console.error('\nRecovery steps:');
error.recovery.forEach((step, index) => {
console.error(`${index + 1}. ${step}`);
});
console.error(`\nFor more information: ${error.docLink}`);
}
/**
* Attempt to recover from an error
* @param {Object} error Enhanced error object
* @param {string} fallbackType Type of fallback to generate
* @param {string} outputPath Path to write fallback output
* @returns {Object} Success status and path to fallback if created
*/
attemptRecovery(error, fallbackType, outputPath) {
if (!this.autoRecover) {
return { success: false };
}
// Select appropriate recovery strategy based on error category
switch (error.category) {
case ErrorCategory.FILESYSTEM:
return this.recoverFromFilesystemError(error, fallbackType, outputPath);
case ErrorCategory.NETWORK:
return this.recoverFromNetworkError(error);
case ErrorCategory.GENERATION:
return this.recoverFromGenerationError(error, fallbackType, outputPath);
default:
// No specific recovery strategy for other categories
return { success: false };
}
}
/**
* Recover from filesystem errors
* @param {Object} error Enhanced error object
* @param {string} fallbackType Type of fallback to generate
* @param {string} outputPath Path to write fallback output
* @returns {Object} Success status and path to fallback if created
*/
recoverFromFilesystemError(error, fallbackType, outputPath) {
// If there's an output path issue, try to create the directory
if (outputPath && error.message.includes('no such file or directory')) {
try {
const outputDir = path.dirname(outputPath);
PathUtils.ensureDir(outputDir);
console.log(`Created missing directory: ${outputDir}`);
// If fallback type is provided, create a fallback file
if (fallbackType && FALLBACK_TEMPLATES[fallbackType]) {
const template = FALLBACK_TEMPLATES[fallbackType];
FileUtils.writeFile(outputPath, template);
console.log(`Created fallback ${fallbackType} at: ${outputPath}`);
return { success: true, fallbackPath: outputPath };
}
return { success: true };
} catch (recoveryError) {
console.error('Recovery attempt failed:', recoveryError.message);
return { success: false };
}
}
return { success: false };
}
/**
* Recover from network errors
* @param {Object} error Enhanced error object
* @returns {Object} Success status
*/
recoverFromNetworkError(error) {
// Simple retry logic for network errors
// In a full implementation, this would include retry with backoff
return { success: false };
}
/**
* Recover from generation errors
* @param {Object} error Enhanced error object
* @param {string} fallbackType Type of fallback to generate
* @param {string} outputPath Path to write fallback output
* @returns {Object} Success status and path to fallback if created
*/
recoverFromGenerationError(error, fallbackType, outputPath) {
// If we have a fallback type and output path, create a fallback file
if (fallbackType && outputPath && FALLBACK_TEMPLATES[fallbackType]) {
try {
const outputDir = path.dirname(outputPath);
PathUtils.ensureDir(outputDir);
const template = FALLBACK_TEMPLATES[fallbackType];
FileUtils.writeFile(outputPath, template);
console.log(`Created fallback ${fallbackType} at: ${outputPath}`);
return { success: true, fallbackPath: outputPath };
} catch (recoveryError) {
console.error('Recovery attempt failed:', recoveryError.message);
return { success: false };
}
}
return { success: false };
}
/**
* Log an error to the error log file
* @param {Object} error Enhanced error object
*/
logError(error) {
try {
const logDir = path.dirname(this.logPath);
if (!fs.existsSync(logDir)) {
PathUtils.ensureDir(logDir);
}
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
code: error.code,
category: error.category,
message: error.message,
context: error.context,
stack: error.originalError?.stack
};
const logLine = JSON.stringify(logEntry) + '\n';
fs.appendFileSync(this.logPath, logLine);
} catch (logError) {
console.error('Failed to write to error log:', logError.message);
}
}
/**
* Categorize an error based on its message and context
* @param {string} errorMessage Error message
* @param {string} operation Operation that caused the error
* @returns {string} Error category
*/
categorizeError(errorMessage, operation) {
const lowerMessage = errorMessage.toLowerCase();
// Filesystem errors
if (lowerMessage.includes('enoent') ||
lowerMessage.includes('no such file') ||
lowerMessage.includes('directory') ||
lowerMessage.includes('path') ||
lowerMessage.includes('file not found')) {
return ErrorCategory.FILESYSTEM;
}
// Network errors
if (lowerMessage.includes('network') ||
lowerMessage.includes('connection') ||
lowerMessage.includes('timeout') ||
lowerMessage.includes('socket') ||
lowerMessage.includes('request failed') ||
lowerMessage.includes('fetch') ||
lowerMessage.includes('api')) {
return ErrorCategory.NETWORK;
}
// Configuration errors
if (lowerMessage.includes('config') ||
lowerMessage.includes('settings') ||
lowerMessage.includes('options') ||
lowerMessage.includes('environment')) {
return ErrorCategory.CONFIG;
}
// Validation errors
if (lowerMessage.includes('validation') ||
lowerMessage.includes('invalid') ||
lowerMessage.includes('schema') ||
lowerMessage.includes('required field')) {
return ErrorCategory.VALIDATION;
}
// Permission errors
if (lowerMessage.includes('permission') ||
lowerMessage.includes('eacces') ||
lowerMessage.includes('access denied') ||
lowerMessage.includes('not authorized')) {
return ErrorCategory.PERMISSION;
}
// Operation-specific categorization
if (operation.includes('gen')) {
return ErrorCategory.GENERATION;
}
if (operation.includes('run') || operation.includes('exec')) {
return ErrorCategory.EXECUTION;
}
if (operation.includes('security') || operation.includes('analyze')) {
return ErrorCategory.SECURITY;
}
// Default category
return ErrorCategory.UNKNOWN;
}
/**
* Generate a unique error code for tracking and documentation
* @param {string} category Error category
* @param {string} operation Operation that caused the error
* @returns {string} Error code
*/
generateErrorCode(category, operation) {
const categoryPrefix = category.substring(0, 2).toUpperCase();
const operationHash = this.hashString(operation) % 1000;
const timestamp = Date.now() % 10000;
return `${categoryPrefix}-${operationHash.toString().padStart(3, '0')}-${timestamp}`;
}
/**
* Get recovery steps based on error category and context
* @param {string} category Error category
* @param {string} operation Operation that caused the error
* @param {Object} context Additional context information
* @returns {string[]} Recovery steps
*/
getRecoverySteps(category, operation, context) {
switch (category) {
case ErrorCategory.FILESYSTEM:
return [
'Check if the file or directory exists',
'Ensure you have permission to access the location',
'Try specifying an absolute path instead of a relative path',
'Create any missing directories manually'
];
case ErrorCategory.NETWORK:
return [
'Check your internet connection',
'Verify the API endpoint is correct and accessible',
'Check if authentication credentials are valid',
'Try increasing the timeout value',
'Check if a proxy or firewall is blocking the connection'
];
case ErrorCategory.CONFIG:
return [
'Verify your .ctrlshiftleft/config.js file exists and is valid',
'Check environment variables required by the operation',
'Ensure configuration paths are correct for your platform',
'Try running with --reset-config to use default configuration'
];
case ErrorCategory.PERMISSION:
return [
'Check file and directory permissions',
'Try running with elevated privileges if appropriate',
'Ensure your user account has access to the resources',
'On Windows, check if files are locked by another process'
];
case ErrorCategory.GENERATION:
return [
'Check if the input file exists and contains valid code',
'Ensure the component follows standard patterns',
'Try simplifying the component if it is complex',
'Use --output to specify a writable location for generated files'
];
case ErrorCategory.EXECUTION:
return [
'Check if the test framework is properly installed',
'Ensure required browsers or drivers are available',
'Check for environment-specific configuration issues',
'Try running with --verbose for more detailed output'
];
case ErrorCategory.SECURITY:
return [
'Check if the file to analyze exists and is readable',
'Ensure OPENAI_API_KEY is set if using AI features',
'Try running with --pattern-only to use only pattern-based analysis',
'Use --output to specify a writable location for the report'
];
case ErrorCategory.FRAMEWORK:
return [
'Ensure your project uses a supported framework',
'Check if framework-specific configuration exists',
'Try running the framework-specific setup command',
'Update to the latest version of the framework'
];
default:
return [
'Check the command syntax and arguments',
'Try updating ctrl.shift.left to the latest version',
'Run with --verbose for more detailed error information',
'Check the documentation for usage examples'
];
}
}
/**
* Get documentation link for error category
* @param {string} category Error category
* @param {string} code Error code
* @returns {string} Documentation link
*/
getDocumentationLink(category, code) {
return `https://github.com/johngaspar/ctrlshiftleft/docs/troubleshooting#${category}-${code.toLowerCase()}`;
}
/**
* Simple string hash function for generating error codes
* @param {string} str String to hash
* @returns {number} Hash value
*/
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash);
}
}
// Export singleton instance
const errorHandler = new ErrorHandler();
module.exports = {
ErrorCategory,
FALLBACK_TEMPLATES,
ErrorHandler,
errorHandler
};