UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

645 lines (553 loc) 23 kB
/** * ctrl.shift.left Diagnostics Utilities * * A comprehensive set of tools to diagnose and fix common issues with * ctrl.shift.left installation and configuration. */ const fs = require('fs'); const path = require('path'); const { execSync, spawn } = require('child_process'); const chalk = require('chalk'); /** * Console formatting utilities */ const log = { info: (msg) => console.log(chalk.blue('ℹ️ ') + msg), success: (msg) => console.log(chalk.green('✅ ') + msg), warning: (msg) => console.log(chalk.yellow('⚠️ ') + msg), error: (msg) => console.log(chalk.red('❌ ') + msg), header: (msg) => console.log('\n' + chalk.bold.blue(msg) + '\n') }; /** * Core diagnostic tool for ctrl.shift.left */ const diagnosticsTool = { /** * Check if the ctrl.shift.left installation is complete and correct * @param {string} [projectPath=process.cwd()] Path to the project * @returns {Object} Diagnostic results */ checkInstallation: (projectPath = process.cwd()) => { log.header('Checking Installation'); const results = { isInstalled: false, issues: [], missingFiles: [], suggestion: '' }; try { // Check if package.json exists and includes ctrlshiftleft const packageJsonPath = path.join(projectPath, 'package.json'); let hasPackageReference = false; if (fs.existsSync(packageJsonPath)) { try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); hasPackageReference = !!( (packageJson.dependencies && packageJson.dependencies.ctrlshiftleft) || (packageJson.devDependencies && packageJson.devDependencies.ctrlshiftleft) ); if (!hasPackageReference) { results.issues.push('ctrlshiftleft is not listed in package.json dependencies'); } else { log.success('Found ctrlshiftleft in package.json'); } } catch (error) { results.issues.push(`Error reading package.json: ${error.message}`); } } else { results.issues.push('package.json not found'); } // Check for required files const requiredPaths = [ 'node_modules/ctrlshiftleft/package.json', 'node_modules/ctrlshiftleft/bin/ctrlshiftleft', 'node_modules/ctrlshiftleft/bin/ctrlshiftleft-ai', 'node_modules/ctrlshiftleft/vscode-ext-test/generate-tests.js' ]; let missingFiles = []; for (const relPath of requiredPaths) { const fullPath = path.join(projectPath, relPath); if (!fs.existsSync(fullPath)) { missingFiles.push(relPath); } } if (missingFiles.length === 0) { log.success('All required files are present'); results.isInstalled = true; } else { results.missingFiles = missingFiles; log.warning('Missing required files:'); for (const file of missingFiles) { log.warning(` - ${file}`); } results.issues.push(`Missing ${missingFiles.length} required files`); } // Test command execution try { // First try local npx to avoid global permission issues const version = execSync('npx ctrlshiftleft --version', { cwd: projectPath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); log.success(`ctrl.shift.left version: ${version}`); } catch (error) { results.issues.push(`Failed to run ctrlshiftleft command: ${error.message}`); try { // Check if global installation works const globalVersion = execSync('ctrlshiftleft --version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); log.success(`Global ctrl.shift.left version: ${globalVersion}`); log.warning('Global installation works, but local npx fails'); results.suggestion = 'Local installation issue, but global command works. Try using global command with full path.'; } catch (globalError) { log.error('Neither local nor global ctrlshiftleft commands work'); results.suggestion = 'Reinstall the package using "npm install --save-dev ctrlshiftleft" and run the repair script.'; } } // Final determination if (results.issues.length === 0) { results.isInstalled = true; log.success('Installation is complete and correct'); } else { results.isInstalled = false; log.warning(`Found ${results.issues.length} installation issues`); } } catch (error) { results.issues.push(`Unexpected error during diagnostics: ${error.message}`); log.error(`Diagnostics failed: ${error.message}`); } return results; }, /** * Fix common installation issues automatically * @param {string} [projectPath=process.cwd()] Path to the project * @returns {Object} Results of repair attempts */ fixCommonIssues: (projectPath = process.cwd()) => { log.header('Fixing Common Issues'); const results = { repairAttempted: false, repairsPerformed: [], successful: false }; try { // First check what's missing const diagnosis = diagnosticsTool.checkInstallation(projectPath); if (diagnosis.isInstalled) { log.success('No issues to fix!'); results.successful = true; return results; } log.info('Attempting to fix installation issues...'); results.repairAttempted = true; // Create any missing directories if (diagnosis.missingFiles.some(f => f.includes('vscode-ext-test'))) { const vscodePath = path.join(projectPath, 'node_modules/ctrlshiftleft/vscode-ext-test'); if (!fs.existsSync(vscodePath)) { fs.mkdirSync(vscodePath, { recursive: true }); log.success('Created missing directory: vscode-ext-test'); results.repairsPerformed.push('Created missing directory: vscode-ext-test'); } } // If key files are missing, try to copy from global installation if (diagnosis.missingFiles.length > 0) { log.info('Attempting to copy files from global installation...'); // Try to find global installation let globalInstallPath = ''; try { const which = process.platform === 'win32' ? 'where' : 'which'; const globalPath = execSync(`${which} ctrlshiftleft`, { encoding: 'utf8' }).trim(); // Get the path to the actual package if (globalPath) { globalInstallPath = path.resolve(path.dirname(globalPath), '..'); log.success(`Found global installation at: ${globalInstallPath}`); } } catch (error) { log.warning('Could not find global installation'); // Try common global paths const userDir = process.env.HOME || process.env.USERPROFILE; const commonPaths = [ path.join(userDir, '.npm-global', 'lib', 'node_modules', 'ctrlshiftleft'), '/usr/local/lib/node_modules/ctrlshiftleft', '/opt/homebrew/lib/node_modules/ctrlshiftleft', path.join(userDir, 'AppData', 'Roaming', 'npm', 'node_modules', 'ctrlshiftleft') ]; for (const commonPath of commonPaths) { if (fs.existsSync(commonPath)) { globalInstallPath = commonPath; log.success(`Found global installation at common path: ${globalInstallPath}`); break; } } } if (globalInstallPath) { // Copy missing files from global installation for (const missingFile of diagnosis.missingFiles) { const destPath = path.join(projectPath, missingFile); const sourcePath = path.join(globalInstallPath, missingFile.replace('node_modules/ctrlshiftleft/', '')); if (fs.existsSync(sourcePath)) { try { // Create parent directories if needed const parentDir = path.dirname(destPath); if (!fs.existsSync(parentDir)) { fs.mkdirSync(parentDir, { recursive: true }); } fs.copyFileSync(sourcePath, destPath); log.success(`Copied ${path.basename(missingFile)} from global installation`); results.repairsPerformed.push(`Copied ${missingFile} from global installation`); } catch (copyError) { log.error(`Failed to copy ${missingFile}: ${copyError.message}`); } } else { log.warning(`Source file not found in global installation: ${sourcePath}`); } } } else { log.warning('Could not find global installation to copy from'); // As a last resort, try reinstalling log.info('Attempting to reinstall the package locally...'); try { execSync('npm install --save-dev ctrlshiftleft@latest', { cwd: projectPath, stdio: 'inherit' }); log.success('Reinstalled ctrlshiftleft package'); results.repairsPerformed.push('Reinstalled ctrlshiftleft package'); } catch (installError) { log.error(`Reinstall failed: ${installError.message}`); } } } // Create symlinks to ensure CLI commands work try { const moduleDir = path.join(projectPath, 'node_modules/ctrlshiftleft'); const binDir = path.join(projectPath, 'node_modules/.bin'); if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); } const cliCommands = ['ctrlshiftleft', 'ctrlshiftleft-ai', 'ctrlshiftleft-watch-ai', 'ctrlshiftleft-cursor']; for (const cmd of cliCommands) { const sourceBin = path.join(moduleDir, 'bin', cmd); const targetBin = path.join(binDir, cmd); if (fs.existsSync(sourceBin) && !fs.existsSync(targetBin)) { // On Windows, copy instead of symlink if (process.platform === 'win32') { fs.copyFileSync(sourceBin, targetBin); } else { fs.symlinkSync(sourceBin, targetBin); } log.success(`Created CLI link for: ${cmd}`); results.repairsPerformed.push(`Created CLI link for: ${cmd}`); } } } catch (error) { log.error(`Failed to create CLI links: ${error.message}`); } // Check if repairs were successful const finalDiagnosis = diagnosticsTool.checkInstallation(projectPath); results.successful = finalDiagnosis.isInstalled; if (results.successful) { log.success('Repair successful!'); } else { log.warning('Repair was only partially successful'); // Provide detailed instructions for manual repair log.info('Please try the following manual repair steps:'); console.log(` 1. Remove the existing installation: rm -rf node_modules/ctrlshiftleft 2. Reinstall the package: npm install --save-dev ctrlshiftleft@latest 3. Run the setup-nextjs.js script: node node_modules/ctrlshiftleft/scripts/setup-nextjs.js 4. If issues persist, please report them at: https://github.com/johngaspar/ctrlshiftleft/issues `); } } catch (error) { log.error(`Error during repair: ${error.message}`); results.successful = false; } return results; }, /** * Check for framework-specific issues and optimize for the framework * @param {string} [projectPath=process.cwd()] Path to the project * @returns {Object} Results with framework-specific diagnostics */ checkFrameworkCompatibility: (projectPath = process.cwd()) => { log.header('Checking Framework Compatibility'); const results = { detectedFramework: 'unknown', isCompatible: false, optimizationNeeded: false, issues: [] }; try { // Try to detect the framework let framework = 'unknown'; // Check for Next.js if ( fs.existsSync(path.join(projectPath, 'next.config.js')) || fs.existsSync(path.join(projectPath, 'next.config.mjs')) || fs.existsSync(path.join(projectPath, 'app')) || fs.existsSync(path.join(projectPath, 'pages')) ) { framework = 'next.js'; log.info('Detected Next.js project'); } // Check for React else if ( fs.existsSync(path.join(projectPath, 'src', 'App.js')) || fs.existsSync(path.join(projectPath, 'src', 'App.jsx')) || fs.existsSync(path.join(projectPath, 'src', 'App.tsx')) ) { framework = 'react'; log.info('Detected React project'); } // Check for Angular else if (fs.existsSync(path.join(projectPath, 'angular.json'))) { framework = 'angular'; log.info('Detected Angular project'); } // Check for Vue else if ( fs.existsSync(path.join(projectPath, 'vue.config.js')) || fs.existsSync(path.join(projectPath, 'nuxt.config.js')) ) { framework = 'vue'; log.info('Detected Vue/Nuxt project'); } results.detectedFramework = framework; // Check if we have an adapter for this framework let hasAdapter = false; try { // Try to require the adapter const adapterPath = path.join(__dirname, '..', '..', 'frameworks', `${framework.replace('.', '')}-adapter.js`); if (fs.existsSync(adapterPath)) { hasAdapter = true; log.success(`Found adapter for ${framework}`); } else { log.warning(`No specialized adapter found for ${framework}`); results.issues.push(`No specialized adapter for ${framework}`); } } catch (error) { log.warning(`Error loading adapter: ${error.message}`); results.issues.push(`Error loading adapter: ${error.message}`); } // Next.js-specific checks if (framework === 'next.js') { // Load the NextJS adapter try { const NextJSAdapter = require('../../frameworks/nextjs-adapter'); const nextInfo = NextJSAdapter.detectFramework(); if (nextInfo.hasAppRouter) { log.info('Detected Next.js App Router'); // Check for typical issues with App Router if (!fs.existsSync(path.join(projectPath, 'jest.config.js'))) { log.warning('Missing jest.config.js - this is recommended for Next.js App Router'); results.issues.push('Missing jest.config.js'); results.optimizationNeeded = true; } } // Check for test directory structure const testDirs = ['__tests__', 'tests', 'test']; let hasTestDir = false; for (const dir of testDirs) { if (fs.existsSync(path.join(projectPath, dir))) { hasTestDir = true; break; } } if (!hasTestDir) { log.warning('No test directory found - recommend creating __tests__ directory'); results.issues.push('Missing test directory'); results.optimizationNeeded = true; } results.isCompatible = true; } catch (error) { log.warning(`Error during Next.js compatibility check: ${error.message}`); results.issues.push(`Error during compatibility check: ${error.message}`); } } else { // Generic compatibility check for other frameworks results.isCompatible = true; log.info(`${framework} is supported, but may require framework-specific configuration`); } // Final compatibility assessment if (results.isCompatible) { if (results.optimizationNeeded) { log.warning('Framework is compatible but needs optimization'); } else { log.success('Framework is fully compatible'); } } else { log.error('Framework is not fully compatible'); } } catch (error) { log.error(`Error checking framework compatibility: ${error.message}`); results.issues.push(`Unexpected error: ${error.message}`); } return results; }, /** * Run a comprehensive diagnostic scan of the project * @param {string} [projectPath=process.cwd()] Path to the project * @returns {Object} Complete diagnostic results */ runComprehensiveScan: (projectPath = process.cwd()) => { log.header('Running Comprehensive Diagnostic Scan'); const results = { installation: {}, framework: {}, environment: {}, recommendedFixes: [] }; // Check installation results.installation = diagnosticsTool.checkInstallation(projectPath); // Check framework compatibility results.framework = diagnosticsTool.checkFrameworkCompatibility(projectPath); // Check environment log.header('Checking Environment'); try { results.environment.nodeVersion = process.version; log.info(`Node.js version: ${results.environment.nodeVersion}`); // Check for critical environment variables results.environment.hasOpenAIKey = !!process.env.OPENAI_API_KEY; if (results.environment.hasOpenAIKey) { log.success('OPENAI_API_KEY environment variable is set'); } else { log.warning('OPENAI_API_KEY environment variable is not set - AI features will be limited'); results.recommendedFixes.push('Set OPENAI_API_KEY environment variable for AI-enhanced features'); } // Check npm version try { results.environment.npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim(); log.info(`npm version: ${results.environment.npmVersion}`); } catch (error) { log.warning(`Could not determine npm version: ${error.message}`); } // Check disk space if (process.platform !== 'win32') { try { const dfOutput = execSync('df -h .', { encoding: 'utf8' }).trim(); const lines = dfOutput.split('\n'); if (lines.length >= 2) { const diskInfo = lines[1].split(/\s+/); results.environment.diskSpace = diskInfo[4]; // Use percentage log.info(`Disk space used: ${results.environment.diskSpace}`); // Parse percentage as number const diskPercent = parseInt(results.environment.diskSpace.replace('%', '')); if (diskPercent > 90) { log.warning('Disk space is critically low - this may affect performance'); results.recommendedFixes.push('Free up disk space - less than 10% available'); } } } catch (error) { log.warning(`Could not determine disk space: ${error.message}`); } } } catch (error) { log.error(`Error checking environment: ${error.message}`); } // Generate recommended fixes if (!results.installation.isInstalled) { results.recommendedFixes.push('Run repair to fix installation issues'); } if (results.framework.optimizationNeeded) { results.recommendedFixes.push(`Run framework-specific setup for ${results.framework.detectedFramework}`); } return results; }, /** * Check if a specific framework is supported and provide guidance * @param {string} frameworkName Name of the framework to check * @returns {Object} Framework support information */ checkFrameworkSupport: (frameworkName) => { const supportInfo = { isSupported: false, level: 'unsupported', setupCommands: [], notes: [] }; frameworkName = frameworkName.toLowerCase(); // Define supported frameworks and their levels const supportedFrameworks = { 'next.js': { level: 'full', setupCommands: [ 'npx ctrlshiftleft-setup-nextjs', 'npm install --save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom' ], notes: [ 'Full support for both App Router and Pages Router', 'Specialized path resolution for Next.js imports', 'Automatic mocking of Next.js components like Image and Link' ] }, 'react': { level: 'full', setupCommands: [ 'npx ctrlshiftleft-setup-react', 'npm install --save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom' ], notes: [ 'Full support for Create React App and custom React setups', 'Compatible with React v16.8+ (hooks)' ] }, 'angular': { level: 'basic', setupCommands: [ 'npm install --save-dev ctrlshiftleft' ], notes: [ 'Basic support for components', 'Might require manual adjustments for Angular-specific features' ] }, 'vue': { level: 'basic', setupCommands: [ 'npm install --save-dev ctrlshiftleft' ], notes: [ 'Basic support for Vue components', 'Limited integration with Vue-specific testing tools' ] }, 'svelte': { level: 'experimental', setupCommands: [ 'npm install --save-dev ctrlshiftleft' ], notes: [ 'Experimental support', 'May require significant manual adjustments' ] } }; // Check if the framework is supported if (supportedFrameworks[frameworkName]) { const support = supportedFrameworks[frameworkName]; supportInfo.isSupported = true; supportInfo.level = support.level; supportInfo.setupCommands = support.setupCommands; supportInfo.notes = support.notes; log.info(`${frameworkName} support level: ${support.level}`); for (const note of support.notes) { log.info(` - ${note}`); } } else { log.warning(`${frameworkName} is not officially supported`); supportInfo.notes.push('Not officially supported, but generic testing may work'); supportInfo.setupCommands.push('npm install --save-dev ctrlshiftleft'); } return supportInfo; } }; // Export all diagnostic utilities module.exports = diagnosticsTool;