ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
645 lines (553 loc) • 23 kB
JavaScript
/**
* 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;