UNPKG

pury

Version:

šŸ›”ļø AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis

305 lines • 11.7 kB
import { Command } from 'commander'; import { resolve, join } from 'path'; import { promises as fs } from 'fs'; import { logger } from '../../utils/logger.js'; import { fileExists } from '../../utils/file-utils.js'; export function createEnvFormatCommand() { return new Command('env-format') .description('Format and organize environment variable files (.env)') .argument('[path]', 'Path to .env file or directory containing .env files', '.') .option('--apply', 'Actually apply the changes (default is dry-run)') .option('--backup', 'Create backup files before modifying') .option('--sort', 'Sort variables alphabetically', true) .option('--group', 'Group related variables together', true) .option('--validate', 'Validate environment variable formats') .action(async (path, options) => { try { await runEnvFormat(path, options); } catch (error) { logger.error(`Environment formatting failed: ${error.message}`); process.exit(1); } }); } async function runEnvFormat(envPath, options) { const resolvedPath = resolve(envPath); if (!(await fileExists(resolvedPath))) { throw new Error(`Path does not exist: ${resolvedPath}`); } logger.info(`${options.apply ? 'Formatting' : 'Analyzing'} environment files in: ${resolvedPath}`); const spinner = logger.spinner('Finding .env files...'); try { // Find .env files const envFiles = await findEnvFiles(resolvedPath); spinner.succeed(`Found ${envFiles.length} environment files`); if (envFiles.length === 0) { logger.warn('No .env files found'); return; } let totalChanges = 0; const results = []; // Process each .env file for (const envFile of envFiles) { logger.info(`\\nProcessing: ${envFile}`); const content = await fs.readFile(envFile, 'utf8'); const formatResult = formatEnvFile(content, options); results.push({ file: envFile, changes: formatResult.changes, issues: formatResult.issues, preview: formatResult.formatted }); totalChanges += formatResult.changes; // Show issues if validation is enabled if (options.validate && formatResult.issues.length > 0) { logger.warn(` āš ļø Validation issues found:`); for (const issue of formatResult.issues) { logger.warn(` • ${issue}`); } } if (formatResult.changes > 0) { logger.info(` šŸ“ ${formatResult.changes} formatting improvements identified`); } else { logger.success(` āœ… Already well formatted`); } } // Show summary logger.info(`\\nšŸ“Š Summary:`); logger.info(`šŸ“ Files processed: ${envFiles.length}`); logger.info(`šŸ“ Total improvements: ${totalChanges}`); if (totalChanges > 0) { // Show preview of changes logger.info('\\nšŸ” Preview of formatting improvements:'); for (const result of results) { if (result.changes > 0) { logger.info(`\\nšŸ“ ${result.file}:`); logger.info(result.preview .split('\\n') .slice(0, 10) .map(line => ` ${line}`) .join('\\n')); if (result.preview.split('\\n').length > 10) { logger.info(' ...'); } } } if (options.apply) { // Apply changes logger.info('\\nApplying formatting changes...'); const applySpinner = logger.spinner('Formatting files...'); try { for (const result of results) { if (result.changes > 0) { if (options.backup) { await fs.writeFile(`${result.file}.backup`, await fs.readFile(result.file, 'utf8')); } await fs.writeFile(result.file, result.preview); } } applySpinner.succeed(`Successfully formatted ${results.filter(r => r.changes > 0).length} files`); logger.success(`\\nāœ… Environment file formatting completed!`); if (options.backup) { logger.info('šŸ’¾ Backup files created with .backup extension'); } } catch (error) { applySpinner.fail('Failed to apply formatting'); throw error; } } else { logger.info('\\nšŸ’” This was a dry run. Use --apply to actually format the files.'); logger.info('šŸ’” Use --backup to create backup files before modifying.'); } } else { logger.success('\\nāœ… All environment files are already well formatted!'); } } catch (error) { spinner.fail('Failed to process environment files'); throw error; } } async function findEnvFiles(path) { const envFiles = []; try { const stat = await fs.stat(path); if (stat.isFile()) { if (path.endsWith('.env') || path.includes('.env.')) { envFiles.push(path); } } else if (stat.isDirectory()) { const files = await fs.readdir(path); for (const file of files) { const filePath = join(path, file); const fileStat = await fs.stat(filePath); if (fileStat.isFile() && (file === '.env' || file.startsWith('.env.'))) { envFiles.push(filePath); } } } } catch (error) { // Ignore errors for files we can't access } return envFiles; } function formatEnvFile(content, options) { const lines = content.split('\\n'); const issues = []; let changes = 0; // Parse environment variables const variables = []; const comments = []; lines.forEach((line, index) => { const trimmedLine = line.trim(); if (!trimmedLine || trimmedLine.startsWith('#')) { // Comment or empty line if (trimmedLine.startsWith('#')) { comments.push({ text: trimmedLine, line: index }); } return; } // Parse variable assignment const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line); if (match) { const [, key, value] = match; if (key && value !== undefined) { // Validate variable name if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) { issues.push(`Variable "${key}" should use UPPERCASE_SNAKE_CASE naming`); } // Categorize variable const category = categorizeEnvVariable(key); variables.push({ key, value: value.trim(), originalLine: index, category }); } } else { issues.push(`Line ${index + 1}: Invalid environment variable format`); } }); // Format the file const sections = groupVariables(variables, options.group); const formatted = formatSections(sections, options.sort); // Count changes (simplified - comparing line count and order) if (formatted.trim() !== content.trim()) { changes = Math.abs(formatted.split('\\n').length - lines.length) + 1; } return { formatted, changes, issues }; } function categorizeEnvVariable(key) { const categories = { Database: /^(DB_|DATABASE_|MONGO_|POSTGRES_|MYSQL_|REDIS_)/i, 'API Keys': /^(API_|KEY_|SECRET_|TOKEN_)/i, 'AWS/Cloud': /^(AWS_|AZURE_|GCP_|CLOUD_)/i, 'Server/Network': /^(HOST|PORT|URL|DOMAIN|SSL_|TLS_)/i, Authentication: /^(AUTH_|JWT_|OAUTH_|SESSION_)/i, 'Email/SMS': /^(MAIL_|EMAIL_|SMTP_|SMS_|TWILIO_)/i, Environment: /^(NODE_ENV|ENVIRONMENT|ENV|DEBUG)/i, Logging: /^(LOG_|LOGGER_)/i, Cache: /^(CACHE_|MEMCACHED_)/i, Other: /.*/ }; for (const [category, pattern] of Object.entries(categories)) { if (pattern.test(key)) { return category; } } return 'Other'; } function groupVariables(variables, shouldGroup) { if (!shouldGroup) { return { 'All Variables': variables }; } const groups = {}; for (const variable of variables) { const category = variable.category || 'Other'; if (!groups[category]) { groups[category] = []; } groups[category].push(variable); } return groups; } function formatSections(sections, shouldSort) { const output = []; // Define section order for better organization const sectionOrder = [ 'Environment', 'Server/Network', 'Database', 'Authentication', 'API Keys', 'AWS/Cloud', 'Email/SMS', 'Cache', 'Logging', 'Other' ]; const sectionsToProcess = sectionOrder.filter(section => sections[section]); // Add any sections not in the predefined order for (const section of Object.keys(sections)) { if (!sectionOrder.includes(section)) { sectionsToProcess.push(section); } } for (const sectionName of sectionsToProcess) { const variables = sections[sectionName]; if (!variables || variables.length === 0) continue; // Add section header (only if grouping multiple sections) if (Object.keys(sections).length > 1) { output.push(`# ${sectionName}`); } // Sort variables within section if requested const sortedVariables = shouldSort ? [...variables].sort((a, b) => a.key.localeCompare(b.key)) : variables; // Add variables for (const variable of sortedVariables) { const formattedValue = formatEnvValue(variable.value); output.push(`${variable.key}=${formattedValue}`); } // Add blank line between sections if (Object.keys(sections).length > 1) { output.push(''); } } // Remove trailing empty lines while (output.length > 0 && output[output.length - 1] === '') { output.pop(); } return `${output.join('\\n')}\\n`; } function formatEnvValue(value) { // Remove surrounding quotes if they exist and aren't necessary if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { const unquoted = value.slice(1, -1); // Only keep quotes if the value contains spaces or special characters if (!/[\\s#$&*()\\[\\]{};<>?|]/.test(unquoted)) { return unquoted; } } // Add quotes if value contains spaces or special characters and isn't already quoted if (/[\\s#$&*()\\[\\]{};<>?|]/.test(value) && !((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) { return `"${value}"`; } return value; } //# sourceMappingURL=env-format.js.map