UNPKG

ctrlshiftleft

Version:

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

677 lines (582 loc) 22.3 kB
#!/usr/bin/env node /** * ctrl.shift.left Next.js Setup Script * * This script handles specialized setup for Next.js projects to ensure * perfect compatibility with ctrl.shift.left's testing and security features. */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const chalk = require('chalk'); // Console output helpers 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') }; // Detect Next.js project structure function detectNextJs() { const hasApp = fs.existsSync('./app'); const hasPages = fs.existsSync('./pages'); const hasNextConfig = fs.existsSync('./next.config.js') || fs.existsSync('./next.config.mjs'); return { isNextJs: hasApp || hasPages || hasNextConfig, hasAppRouter: hasApp, hasPagesRouter: hasPages, hasNextConfig, }; } // Install dependencies without requiring global permissions async function installDependencies() { log.header('Installing ctrl.shift.left dependencies'); try { log.info('Installing locally (no global permissions required)...'); execSync('npm install --save-dev ctrlshiftleft@latest', { stdio: 'inherit' }); // Add the .npm-global to user directory for non-sudo global installs const userHome = process.env.HOME || process.env.USERPROFILE; const npmGlobalPath = path.join(userHome, '.npm-global'); if (!fs.existsSync(npmGlobalPath)) { log.info('Creating user-level npm global directory...'); fs.mkdirSync(npmGlobalPath, { recursive: true }); log.info('Adding user-level npm bin to PATH...'); // Create or update .npmrc const npmrcPath = path.join(userHome, '.npmrc'); const npmrcContent = `prefix=${npmGlobalPath}\n`; fs.writeFileSync(npmrcPath, npmrcContent, { flag: 'a' }); // Suggest PATH update for shell profile const shellProfile = process.platform === 'darwin' ? '~/.zshrc or ~/.bash_profile' : '~/.bashrc'; log.warning(`For user-level global installs without sudo, add this to your ${shellProfile}:`); console.log(` export PATH="${npmGlobalPath}/bin:$PATH"`); } log.success('Dependencies installed successfully'); } catch (error) { log.error(`Installation failed: ${error.message}`); log.info('Attempting alternative installation method...'); try { execSync('npm install ctrlshiftleft@latest --no-save', { stdio: 'inherit' }); log.success('Alternative installation successful'); } catch (altError) { log.error(`Alternative installation failed: ${altError.message}`); log.info('Please try installing manually:'); console.log(' npm install --save-dev ctrlshiftleft@latest'); process.exit(1); } } } // Create a minimal validation file as fallback function createMinimalValidationFile(filePath) { // Create the minimal validation file without using template literals // to avoid TypeScript parsing issues const minimalContent = [ '/**', ' * Minimal QA validation utility (fallback version)', ' * Created by ctrl.shift.left setup', ' */', '', '/**', ' * Validates input for common security issues', ' * @param {string} input - The input to validate', ' * @param {string} inputType - Type of input (text, url, email, password, etc.)', ' * @returns {Object} Validation result', ' */', 'function validateInput(input, inputType) {', ' // Basic validation patterns', ' const patterns = {', ' text: /.+/,', ' url: /^https?:\/\/.+/,', ' email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,', ' password: /.{8,}/', ' };', '', ' // Security checks', ' const securityChecks = {', ' text: [', ' { pattern: /(<script|javascript:|on\w+\s*=)/i, message: \'Potential XSS detected\' },', ' { pattern: /(SELECT|INSERT|UPDATE|DELETE|DROP|UNION)\s+/i, message: \'Potential SQL injection detected\' },', ' { pattern: /(\/bin\/|exec\(|eval\()/i, message: \'Potential command injection\' }', ' ],', ' url: [', ' { pattern: /javascript:/i, message: \'Unsafe URL scheme detected\' },', ' { pattern: /data:/i, message: \'Unsafe URL scheme detected\' }', ' ]', ' };', '', ' // Results', ' const result = {', ' valid: true,', ' message: \'\',', ' securityIssues: []', ' };', '', ' // Check basic pattern', ' if (patterns[inputType] && !patterns[inputType].test(input)) {', ' result.valid = false;', ' result.message = `Invalid ${inputType} format`;', ' return result;', ' }', '', ' // Check security issues', ' if (securityChecks[inputType]) {', ' for (const check of securityChecks[inputType]) {', ' if (check.pattern.test(input)) {', ' result.securityIssues.push(check.message);', ' }', ' }', ' }', '', ' result.valid = result.securityIssues.length === 0;', ' if (result.securityIssues.length > 0) {', ' result.message = result.securityIssues[0];', ' }', '', ' return result;', '}', '', 'module.exports = { validateInput };', ].join('\n'); try { fs.writeFileSync(filePath, minimalContent); log.success(`Created minimal fallback file: ${filePath}`); return true; } catch (error) { log.error(`Failed to create minimal file ${filePath}: ${error.message}`); return false; } } // Update environment variables and create paths.js file for module resolution function updateScriptPaths() { const ctrlDirPath = path.join(process.cwd(), '.ctrlshiftleft'); const pathsFile = path.join(ctrlDirPath, 'paths.js'); // Get properly escaped paths const scriptsDir = path.join(ctrlDirPath, 'scripts').replace(/\\/g, '\\\\'); const vscodePath = path.join(process.cwd(), 'node_modules/ctrlshiftleft/vscode-ext-test').replace(/\\/g, '\\\\'); const reportsDir = path.join(process.cwd(), 'reports').replace(/\\/g, '\\\\'); // Create content without template literals to avoid syntax issues const pathsContent = [ '/**', ' * ctrl.shift.left path configuration', ' * Generated by setup script', ' */', '', 'module.exports = {', " SCRIPTS_DIR: '" + scriptsDir + "',", " VSCODE_EXT_DIR: '" + vscodePath + "',", " OUTPUT_DIR: '" + reportsDir + "'", '};' ].join('\n'); try { fs.writeFileSync(pathsFile, pathsContent); log.success(`Created paths configuration: ${pathsFile}`); // Create .env file if it doesn't exist const envPath = path.join(process.cwd(), '.env'); if (!fs.existsSync(envPath)) { fs.writeFileSync(envPath, '# ctrl.shift.left configuration\n' + 'CTRLSHIFTLEFT_SCRIPTS_DIR=' + ctrlDirPath + '/scripts\n'); log.success('Added path configuration to .env file'); } else { // Append to existing .env const envContent = fs.readFileSync(envPath, 'utf8'); if (!envContent.includes('CTRLSHIFTLEFT_SCRIPTS_DIR')) { fs.appendFileSync(envPath, '\n# ctrl.shift.left configuration\n' + 'CTRLSHIFTLEFT_SCRIPTS_DIR=' + ctrlDirPath + '/scripts\n'); log.success('Added path configuration to existing .env file'); } } } catch (error) { log.warning(`Failed to update paths: ${error.message}`); } } // Create proper module resolution paths function ensureCorrectModulePaths() { log.header('Setting up module paths'); // 1. Project node_modules path const modulePath = path.join(process.cwd(), 'node_modules/ctrlshiftleft'); const vscodePath = path.join(modulePath, 'vscode-ext-test'); // 2. Create ctrl.shift.left project-specific directory const ctrlDirPath = path.join(process.cwd(), '.ctrlshiftleft'); const ctrlScriptsPath = path.join(ctrlDirPath, 'scripts'); // 3. Create directory structure for .ctrlshiftleft if (!fs.existsSync(ctrlDirPath)) { log.info('Creating .ctrlshiftleft directory...'); fs.mkdirSync(ctrlDirPath, { recursive: true }); } if (!fs.existsSync(ctrlScriptsPath)) { log.info('Creating .ctrlshiftleft/scripts directory...'); fs.mkdirSync(ctrlScriptsPath, { recursive: true }); } // Create vscode integration directory if needed if (!fs.existsSync(vscodePath)) { log.info('Creating vscode-ext-test directory...'); fs.mkdirSync(vscodePath, { recursive: true }); } // Try multiple potential sources for the files // 1. Try from the loaded package let pkgPath; try { pkgPath = path.dirname(require.resolve('ctrlshiftleft/package.json')); } catch (error) { // 2. Try from global installation try { const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim(); pkgPath = path.join(globalPath, 'ctrlshiftleft'); log.info(`Using global installation at: ${pkgPath}`); } catch (globalError) { // 3. Try from current directory pkgPath = path.join(process.cwd(), 'node_modules/ctrlshiftleft'); log.warning(`Using local fallback path: ${pkgPath}`); } } // Files to copy to ensure availability const sourceFiles = [ // VSCode extension files { source: path.join(pkgPath, 'vscode-ext-test/generate-tests.js'), dest: path.join(vscodePath, 'generate-tests.js') }, { source: path.join(pkgPath, 'vscode-ext-test/analyze-security-enhanced.js'), dest: path.join(vscodePath, 'analyze-security-enhanced.js') }, // .ctrlshiftleft scripts { source: path.join(pkgPath, 'src/middleware/qa-validation.ts'), dest: path.join(ctrlScriptsPath, 'validate-api.js') }, { source: path.join(pkgPath, 'src/middleware/qa-validation.js'), dest: path.join(ctrlScriptsPath, 'validate-api.js') }, ]; let copySuccess = true; // Try to copy each file, with fallbacks for different file paths sourceFiles.forEach(({source, dest}) => { try { // Try original source path if (fs.existsSync(source)) { fs.copyFileSync(source, dest); log.success(`Copied ${path.basename(source)} → ${dest}`); } else { // Try finding alternative source paths const alternatives = [ // Try compiled JS version if TS not found source.replace('.ts', '.js'), // Try dist directory source.replace('src/', 'dist/'), // Try looking in non-minified directory source.replace('/min/', '/'), // Try looking in alternative locations path.join(process.cwd(), 'node_modules/ctrlshiftleft', path.basename(source)), path.join(pkgPath, path.basename(source)) ]; let found = false; for (const alt of alternatives) { if (fs.existsSync(alt)) { fs.copyFileSync(alt, dest); log.success(`Copied alternative: ${path.basename(alt)} → ${dest}`); found = true; break; } } if (!found) { log.warning(`Source file not found: ${source} or any alternatives`); // Try to create a minimal version as last resort for critical files if (path.basename(dest) === 'validate-api.js') { createMinimalValidationFile(dest); } else { copySuccess = false; } } } } catch (error) { log.error(`Failed to copy ${path.basename(source)}: ${error.message}`); // Try to create minimal versions for critical files if (path.basename(dest) === 'validate-api.js') { createMinimalValidationFile(dest); } else { copySuccess = false; } } }); // Create environment variable for custom paths updateScriptPaths(); if (copySuccess) { log.success('All module paths set up correctly'); } else { log.warning('Some module paths could not be set up.'); log.info('Attempting fallback mechanisms...'); // Try to run repair tool try { execSync('npx ctrlshiftleft-repair --fix', { stdio: 'inherit' }); log.success('Repair tool completed'); } catch (error) { log.error(`Repair tool failed: ${error.message}`); log.info('You may need to manually copy files or reinstall ctrlshiftleft with:'); console.log(' npm install --save-dev ctrlshiftleft@latest'); } } // Create symlinks to ensure CLI commands work try { const binDir = path.join(process.cwd(), 'node_modules/.bin'); if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); } const cliCommands = ['ctrlshiftleft', 'ctrlshiftleft-ai', 'ctrlshiftleft-watch-ai', 'ctrlshiftleft-cursor']; cliCommands.forEach(cmd => { const sourceBin = path.join(modulePath, '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(`Linked CLI command: ${cmd}`); } }); } catch (error) { log.error(`Failed to create CLI links: ${error.message}`); } } // Add Next.js specific configuration function configureForNextJs(nextJsInfo) { log.header('Configuring for Next.js'); const packageJsonPath = path.join(process.cwd(), 'package.json'); let packageJson; try { packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); } catch (error) { log.error(`Could not read package.json: ${error.message}`); return; } // Add Next.js specific scripts packageJson.scripts = packageJson.scripts || {}; const appRouterDir = nextJsInfo.hasAppRouter ? './app' : './src'; const componentsDir = nextJsInfo.hasAppRouter ? `${appRouterDir}/components` : './components'; const apiDir = nextJsInfo.hasAppRouter ? `${appRouterDir}/api` : './pages/api'; const scripts = { 'qa:gen': `npx ctrlshiftleft gen ${componentsDir}`, 'qa:analyze': `npx ctrlshiftleft-ai analyze ${appRouterDir}`, 'qa:checklist': `npx ctrlshiftleft checklist ${appRouterDir}`, 'qa:watch': `npx ctrlshiftleft-watch-ai ${appRouterDir}`, 'qa': 'npm run qa:gen && npm run qa:analyze && npm run qa:checklist' }; let scriptsAdded = 0; for (const [key, value] of Object.entries(scripts)) { if (!packageJson.scripts[key]) { packageJson.scripts[key] = value; scriptsAdded++; } } if (scriptsAdded > 0) { try { fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); log.success(`Added ${scriptsAdded} QA scripts to package.json`); } catch (error) { log.error(`Failed to update package.json: ${error.message}`); } } else { log.info('QA scripts already exist in package.json'); } // Create GitHub Actions workflow for Next.js const workflowsDir = path.join(process.cwd(), '.github/workflows'); const workflowPath = path.join(workflowsDir, 'qa.yml'); if (!fs.existsSync(workflowsDir)) { fs.mkdirSync(workflowsDir, { recursive: true }); } if (!fs.existsSync(workflowPath)) { const workflowContent = `name: Quality Assurance on: push: branches: [ main ] pull_request: branches: [ main ] jobs: quality-assurance: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm' - name: Install dependencies run: npm ci --legacy-peer-deps - name: Install ctrl.shift.left run: npm install -g ctrlshiftleft - name: Set up OpenAI API Key if: \${{ secrets.OPENAI_API_KEY != '' }} run: echo "OPENAI_API_KEY=\${{ secrets.OPENAI_API_KEY }}" >> \$GITHUB_ENV - name: Generate tests run: npx ctrlshiftleft gen ${componentsDir} --output=__tests__/components - name: Run security analysis run: npx ctrlshiftleft-ai analyze ${appRouterDir} --output=security-reports `; fs.writeFileSync(workflowPath, workflowContent); log.success('Created GitHub Actions workflow for QA'); } else { log.info('GitHub Actions workflow already exists'); } // Create qa.ts utility module const libDir = path.join(process.cwd(), 'lib'); const qaUtilPath = path.join(libDir, 'qa.ts'); if (!fs.existsSync(libDir)) { fs.mkdirSync(libDir, { recursive: true }); } if (!fs.existsSync(qaUtilPath)) { const qaUtilContent = `/** * RedesignRadar QA Module * Provides utilities for quality assurance and security testing */ /** * Validate user input for common security risks * @param input User input to validate * @param inputType Type of input (e.g., 'url', 'email', 'password') */ export function validateInput(input: string, inputType: 'url' | 'email' | 'password' | 'text' = 'text') { // Basic validation patterns const patterns = { url: /^(https?:\\/\\/)?([\\da-z.-]+)\\.([a-z.]{2,6})([/\\w .-]*)*\\/?$/, email: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/, password: /.{8,}/, // At least 8 characters }; // Check for common security issues const securityChecks = { url: [ { pattern: /<|>|script|on\\w+=/i, message: "URL contains potentially unsafe characters" }, { pattern: /javascript:/i, message: "URL contains JavaScript protocol" }, { pattern: /data:/i, message: "URL contains data protocol" }, ], email: [ { pattern: /<|>|script|on\\w+=/i, message: "Email contains potentially unsafe characters" }, ], password: [ { pattern: /^(password|123456|admin|qwerty)/i, message: "Password is too common" }, ], text: [ { pattern: /<script|javascript:|on\\w+=/i, message: "Text contains potentially unsafe code" }, ], }; // Results const result = { valid: false, message: '', securityIssues: [] as string[], }; // Check basic pattern if available if (patterns[inputType] && !patterns[inputType].test(input)) { result.message = \`Invalid \${inputType} format\`; return result; } // Check security issues if (securityChecks[inputType]) { for (const check of securityChecks[inputType]) { if (check.pattern.test(input)) { result.securityIssues.push(check.message); } } } result.valid = result.securityIssues.length === 0; result.message = result.securityIssues.length > 0 ? result.securityIssues[0] : \`Valid \${inputType}\`; return result; } /** * Validates a URL with security checks */ export function validateUrl(url: string) { const result = validateInput(url, 'url'); return { isValid: result.valid, errorMessage: !result.valid ? result.message : '', hasSecurity: result.securityIssues?.length > 0, securityIssues: result.securityIssues || [] }; } // Export all functions export default { validateInput, validateUrl }; `; fs.writeFileSync(qaUtilPath, qaUtilContent); log.success('Created QA utility module'); } else { log.info('QA utility module already exists'); } } // Run diagnostics to check for issues function runDiagnostics() { log.header('Running diagnostics'); try { // Check if all required files exist const requiredPaths = [ 'node_modules/ctrlshiftleft/vscode-ext-test/generate-tests.js', 'node_modules/ctrlshiftleft/bin/ctrlshiftleft', 'node_modules/ctrlshiftleft/bin/ctrlshiftleft-ai' ]; const issues = requiredPaths.filter(p => !fs.existsSync(path.join(process.cwd(), p))); if (issues.length > 0) { log.warning('Installation issues detected:'); issues.forEach(p => log.warning(` - Missing: ${p}`)); // Try to fix issues log.info('Attempting to fix issues...'); ensureCorrectModulePaths(); } else { log.success('All required files are present'); } // Test a command try { const version = execSync('npx ctrlshiftleft --version', { encoding: 'utf8' }).trim(); log.success(`ctrl.shift.left version: ${version}`); } catch (error) { log.error('Failed to run ctrlshiftleft command'); ensureCorrectModulePaths(); } } catch (error) { log.error(`Error during diagnostics: ${error.message}`); } } // Main function async function main() { log.header('ctrl.shift.left Next.js Setup'); const nextJsInfo = detectNextJs(); if (!nextJsInfo.isNextJs) { log.warning('This does not appear to be a Next.js project.'); const readline = require('readline').createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise(resolve => { readline.question('Continue anyway? (y/n) ', resolve); }); readline.close(); if (answer.toLowerCase() !== 'y') { log.error('Setup aborted.'); process.exit(1); } } else { log.success('Next.js project detected!'); if (nextJsInfo.hasAppRouter) { log.info('App Router structure detected'); } if (nextJsInfo.hasPagesRouter) { log.info('Pages Router structure detected'); } } // Run all setup steps await installDependencies(); ensureCorrectModulePaths(); configureForNextJs(nextJsInfo); runDiagnostics(); log.header('Setup Complete'); log.success('ctrl.shift.left is now configured for your Next.js project!'); log.info('Try running the following commands:'); console.log(' npm run qa:gen -- ./app/components/YourComponent.tsx'); console.log(' npm run qa:analyze'); console.log(' npm run qa:watch'); log.info('If you encounter any issues, please visit:'); console.log(' https://github.com/johngaspar/ctrlshiftleft/issues'); } // Run the script main().catch(error => { log.error(`Setup failed: ${error.message}`); process.exit(1); });