ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
533 lines (446 loc) • 15.1 kB
JavaScript
/**
* NPM Package Preparation Script
*
* This script prepares the ctrl.shift.left package for NPM publication by:
* 1. Creating a clean directory structure
* 2. Copying necessary files
* 3. Ensuring proper bin files are executable
* 4. Generating API documentation
*
* Usage:
* node ./scripts/prepare-npm-package.js
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// ANSI color codes for terminal output
const COLORS = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
underscore: '\x1b[4m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m'
};
// Project paths
const ROOT_DIR = path.resolve(__dirname, '..');
const DIST_DIR = path.join(ROOT_DIR, 'dist');
const BIN_DIR = path.join(ROOT_DIR, 'bin');
const SRC_DIR = path.join(ROOT_DIR, 'src');
const DOCS_DIR = path.join(ROOT_DIR, 'docs');
const EXAMPLES_DIR = path.join(ROOT_DIR, 'examples');
const NPM_PACKAGE_DIR = path.join(ROOT_DIR, 'npm-package');
// Files to copy directly
const FILES_TO_COPY = [
{ source: 'README.md', dest: 'README.md' },
{ source: 'LICENSE', dest: 'LICENSE' },
{ source: 'package.json', dest: 'package.json' },
{ source: '.npmignore', dest: '.npmignore' },
{ source: 'docs/AI_SECURITY_GUIDE.md', dest: 'docs/AI_SECURITY_GUIDE.md' },
{ source: 'docs/CURSOR_INTEGRATION.md', dest: 'docs/CURSOR_INTEGRATION.md' },
{ source: 'examples/vscode-tasks.json', dest: 'examples/vscode-tasks.json' }
];
// Binary files to ensure are executable
const BIN_FILES = [
'ctrlshiftleft',
'ctrlshiftleft-ai',
'ctrlshiftleft-watch-ai',
'ctrlshiftleft-cursor'
];
// Log with colors
function log(message, type = 'info') {
const timestamp = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
switch (type) {
case 'success':
console.log(`${COLORS.green}✓ ${message}${COLORS.reset}`);
break;
case 'error':
console.log(`${COLORS.red}✗ ${timestamp} ERROR: ${message}${COLORS.reset}`);
break;
case 'warning':
console.log(`${COLORS.yellow}⚠ ${timestamp} WARNING: ${message}${COLORS.reset}`);
break;
case 'step':
console.log(`\n${COLORS.cyan}${COLORS.bright}${message}${COLORS.reset}`);
break;
default:
console.log(`${COLORS.dim}${timestamp}${COLORS.reset} ${message}`);
}
}
// Create directory if it doesn't exist
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
log(`Created directory: ${dirPath}`, 'success');
}
}
// Copy file with logging
function copyFile(source, dest) {
try {
fs.copyFileSync(source, dest);
log(`Copied ${source} to ${dest}`, 'success');
} catch (error) {
log(`Failed to copy ${source}: ${error.message}`, 'error');
throw error;
}
}
// Copy directory recursively
function copyDirRecursive(source, dest) {
ensureDir(dest);
const entries = fs.readdirSync(source, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(source, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirRecursive(srcPath, destPath);
} else {
copyFile(srcPath, destPath);
}
}
}
// Run a shell command
function runCommand(command, cwd = ROOT_DIR) {
try {
log(`Running: ${command}`);
return execSync(command, { cwd, stdio: 'inherit' });
} catch (error) {
log(`Command failed: ${command}`, 'error');
throw error;
}
}
// Make a file executable
function makeExecutable(filePath) {
try {
fs.chmodSync(filePath, '755');
log(`Made ${filePath} executable`, 'success');
} catch (error) {
log(`Failed to make ${filePath} executable: ${error.message}`, 'error');
throw error;
}
}
// Clean the distribution directory
function cleanDist() {
log('Cleaning distribution directory', 'step');
if (fs.existsSync(NPM_PACKAGE_DIR)) {
log(`Removing existing npm-package directory: ${NPM_PACKAGE_DIR}`);
fs.rmSync(NPM_PACKAGE_DIR, { recursive: true, force: true });
}
ensureDir(NPM_PACKAGE_DIR);
ensureDir(path.join(NPM_PACKAGE_DIR, 'bin'));
ensureDir(path.join(NPM_PACKAGE_DIR, 'dist'));
ensureDir(path.join(NPM_PACKAGE_DIR, 'docs'));
ensureDir(path.join(NPM_PACKAGE_DIR, 'examples'));
}
// Create bin files
function prepareBinFiles() {
log('Preparing binary files', 'step');
// Copy bin files
const binFiles = fs.readdirSync(BIN_DIR);
for (const file of binFiles) {
const source = path.join(BIN_DIR, file);
const dest = path.join(NPM_PACKAGE_DIR, 'bin', file);
copyFile(source, dest);
makeExecutable(dest);
}
}
// Build the TypeScript source
function buildTypeScript() {
log('Building TypeScript source', 'step');
// First run the TypeScript compiler
runCommand('npm run build');
// Copy the dist files
copyDirRecursive(DIST_DIR, path.join(NPM_PACKAGE_DIR, 'dist'));
// Copy TypeScript declaration files
log('Copying TypeScript declaration files');
// AI security analyzer declaration file
const aiSecurityDtsSource = path.join(SRC_DIR, 'ai-security-analyzer.d.ts');
const aiSecurityDtsDest = path.join(NPM_PACKAGE_DIR, 'dist', 'ai-security-analyzer.d.ts');
if (fs.existsSync(aiSecurityDtsSource)) {
copyFile(aiSecurityDtsSource, aiSecurityDtsDest);
} else {
log(`TypeScript declaration file not found: ${aiSecurityDtsSource}`, 'warning');
}
}
// Copy extra files
function copyExtraFiles() {
log('Copying extra files', 'step');
// Copy AI security analyzer files
log('Copying AI security analyzer files');
const aiSecuritySource = path.join(SRC_DIR, 'ai-security-analyzer.js');
const aiSecurityDest = path.join(NPM_PACKAGE_DIR, 'dist', 'ai-security-analyzer.js');
if (fs.existsSync(aiSecuritySource)) {
copyFile(aiSecuritySource, aiSecurityDest);
} else {
log(`AI security analyzer not found: ${aiSecuritySource}`, 'warning');
}
// Copy cursor integration files
log('Copying cursor integration files');
const cursorIntegrationSource = path.join(ROOT_DIR, 'cursor-integration');
const cursorIntegrationDest = path.join(NPM_PACKAGE_DIR, 'cursor-integration');
if (fs.existsSync(cursorIntegrationSource)) {
fs.mkdirSync(cursorIntegrationDest, { recursive: true });
copyDirRecursive(cursorIntegrationSource, cursorIntegrationDest);
} else {
log(`Cursor integration directory not found: ${cursorIntegrationSource}`, 'warning');
}
// Copy VS Code extension files
log('Copying VS Code extension files');
const vscodeExtSource = path.join(ROOT_DIR, 'vscode-ext-test');
const vscodeExtDest = path.join(NPM_PACKAGE_DIR, 'vscode-ext-test');
if (fs.existsSync(vscodeExtSource)) {
fs.mkdirSync(vscodeExtDest, { recursive: true });
copyDirRecursive(vscodeExtSource, vscodeExtDest);
log('VS Code extension files copied successfully');
} else {
log(`VS Code extension directory not found: ${vscodeExtSource}`, 'warning');
}
// Copy root files
for (const file of FILES_TO_COPY) {
const source = path.join(ROOT_DIR, file.source);
const dest = path.join(NPM_PACKAGE_DIR, file.dest);
if (fs.existsSync(source)) {
copyFile(source, dest);
} else {
log(`File not found: ${source}`, 'warning');
}
}
// Copy documentation
log('Copying documentation');
copyDirRecursive(DOCS_DIR, path.join(NPM_PACKAGE_DIR, 'docs'));
// Copy examples
log('Copying examples');
copyDirRecursive(EXAMPLES_DIR, path.join(NPM_PACKAGE_DIR, 'examples'));
}
// Update package.json for publishing
function updatePackageJson() {
log('Updating package.json for publishing', 'step');
const packageJsonPath = path.join(NPM_PACKAGE_DIR, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
log(`package.json not found: ${packageJsonPath}`, 'error');
return;
}
// Read package.json
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Ensure main, types, and bin paths are correct
packageJson.main = 'dist/api/index.js';
packageJson.types = 'dist/api/index.d.ts';
packageJson.bin = {
'ctrlshiftleft': 'bin/ctrlshiftleft',
'ctrlshiftleft-ai': 'bin/ctrlshiftleft-ai'
};
// Clean up any private or development-specific fields
delete packageJson.private;
// Simplify scripts to only include what's needed after installation
packageJson.scripts = {
"start": "node bin/ctrlshiftleft",
"ai": "node bin/ctrlshiftleft-ai"
};
// Write updated package.json
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
log('Updated package.json', 'success');
}
// Generate npm-shrinkwrap.json for exact dependency versions
function generateShrinkwrap() {
log('Generating npm-shrinkwrap.json', 'step');
// Change to the npm package directory
process.chdir(NPM_PACKAGE_DIR);
// Run npm install to create a package-lock.json
runCommand('npm install --package-lock-only', NPM_PACKAGE_DIR);
// Convert package-lock.json to npm-shrinkwrap.json
runCommand('npm shrinkwrap', NPM_PACKAGE_DIR);
// Change back to the root directory
process.chdir(ROOT_DIR);
log('Generated npm-shrinkwrap.json', 'success');
}
// Create a .npmignore file if it doesn't exist
function createNpmIgnore() {
const npmIgnorePath = path.join(NPM_PACKAGE_DIR, '.npmignore');
if (!fs.existsSync(npmIgnorePath)) {
log('Creating .npmignore', 'step');
const npmIgnoreContent = `
# Development files
*.log
*.tsbuildinfo
.DS_Store
.env
.env.*
.vscode
.idea
.git
# Test files
test/
tests/
__tests__/
*.test.js
*.test.ts
*.spec.js
*.spec.ts
# Build config
tsconfig*.json
rollup.config.js
webpack.config.js
babel.config.js
.eslintrc*
.prettierrc*
# Source files (only distribute dist)
src/
# CI/CD
.github/
.gitlab/
.circleci/
.travis.yml
# Documentation source
docs/source/
`;
fs.writeFileSync(npmIgnorePath, npmIgnoreContent.trim());
log('Created .npmignore', 'success');
}
}
// Build a dry-run package to test
function testBuild() {
log('Testing npm package build', 'step');
// Pack the package
process.chdir(NPM_PACKAGE_DIR);
runCommand('npm pack --dry-run', NPM_PACKAGE_DIR);
process.chdir(ROOT_DIR);
log('npm package dry-run completed successfully', 'success');
}
// Test the enhanced watcher functionality
function testEnhancedWatcherFunctionality() {
log('Testing enhanced watcher functionality', 'step');
try {
// Create a test script to verify the EnhancedWatcher works
const testDir = path.join(NPM_PACKAGE_DIR, 'examples');
ensureDir(testDir);
const testFile = path.join(testDir, 'ai-watcher-example.js');
const testContent = [
'#!/usr/bin/env node',
'',
'/**',
' * AI-Enhanced Watcher Example',
' * ',
' * This example demonstrates how to use the AI-enhanced watcher functionality',
' * from the ctrl.shift.left API to monitor your codebase in real-time.',
' */',
'',
'const { EnhancedWatcher } = require(\'../dist/api\');',
'',
'// Create an EnhancedWatcher instance with custom options',
'const watcher = new EnhancedWatcher({',
' // Include specific file patterns',
' include: ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"],',
' ',
' // Exclude test and build files',
' exclude: ["**/node_modules/**", "**/dist/**", "**/*.test.*"],',
' ',
' // Use AI-enhanced security analysis if OPENAI_API_KEY is available',
' useAIAnalysis: true,',
' ',
' // Generate tests automatically',
' generateTests: true,',
' ',
' // Output directories',
' testOutputDir: "./generated-tests",',
' securityOutputDir: "./security-reports",',
' checklistOutputDir: "./qa-checklists",',
' ',
' // Maximum concurrent tasks',
' maxConcurrentTasks: 2',
'});',
'',
'// Check if AI security analysis is available',
'console.log("AI Security Analysis Available:", watcher.isAIAvailable());',
'',
'// Log event handler setup',
'watcher.on("ready", () => {',
' console.log("Watcher is ready and monitoring for file changes");',
'});',
'',
'watcher.on("file:change", (event) => {',
' console.log(`File ${event.type}: ${event.path}`);',
'});',
'',
'watcher.on("analysis:complete", (result) => {',
' if (result.success) {',
' console.log(`Analysis completed for ${result.filePath}`);',
' if (result.outputPath) {',
' console.log(`Output saved to: ${result.outputPath}`);',
' }',
' } else {',
' console.error(`Analysis failed for ${result.filePath}: ${result.error}`);',
' }',
'});',
'',
'// Start watching the current directory',
'const stopWatching = watcher.watch(process.cwd());',
'',
'console.log("Watcher started. Press Ctrl+C to stop.");',
'',
'// Handle process exit',
'process.on("SIGINT", async () => {',
' console.log("\nStopping watcher...");',
' await watcher.stop();',
' console.log("Watcher stopped.");',
' process.exit(0);',
'});'
].join('\n');
fs.writeFileSync(testFile, testContent);
makeExecutable(testFile);
log('Created AI-enhanced watcher example file', 'success');
} catch (error) {
log(`Testing enhanced watcher failed: ${error.message}`, 'warning');
console.error(error);
}
}
// Main function
async function main() {
log('Starting NPM package preparation', 'step');
try {
// Clean dist directory
cleanDist();
// Build TypeScript source
buildTypeScript();
// Prepare binary files
prepareBinFiles();
// Copy extra files
copyExtraFiles();
// Update package.json
updatePackageJson();
// Create .npmignore
createNpmIgnore();
// Generate npm-shrinkwrap.json
generateShrinkwrap();
// Test enhanced watcher functionality
testEnhancedWatcherFunctionality();
// Test build
testBuild();
log(`
${COLORS.green}${COLORS.bright}NPM package preparation completed successfully!${COLORS.reset}
${COLORS.bright}Package location:${COLORS.reset} ${COLORS.cyan}${NPM_PACKAGE_DIR}${COLORS.reset}
To publish the package:
${COLORS.yellow}cd ${NPM_PACKAGE_DIR}
npm publish${COLORS.reset}
To test the package locally:
${COLORS.yellow}cd ${NPM_PACKAGE_DIR}
npm link
cd /path/to/your/project
npm link ctrlshiftleft${COLORS.reset}
`);
} catch (error) {
log(`NPM package preparation failed: ${error.message}`, 'error');
console.error(error);
process.exit(1);
}
}
// Run the script
main().catch(error => {
log(`Unhandled error: ${error.message}`, 'error');
console.error(error);
process.exit(1);
});