ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
677 lines (582 loc) • 22.3 kB
JavaScript
#!/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);
});