pury
Version:
š”ļø AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis
305 lines ⢠11.7 kB
JavaScript
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