UNPKG

envx-cli

Version:

Environment file encryption and management tool

435 lines 20.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createCopyCommand = void 0; const chalk_1 = __importDefault(require("chalk")); const commander_1 = require("commander"); const path_1 = __importDefault(require("path")); const schemas_1 = require("../schemas"); const exec_1 = require("../utils/exec"); const file_1 = require("../utils/file"); const interactive_1 = require("../utils/interactive"); const createCopyCommand = () => { const command = new commander_1.Command('copy'); command .description('Copy environment file from stage to .env') .option('-e, --environment <env>', 'Environment name (e.g., development, staging, production)') .option('-p, --passphrase <passphrase>', 'Passphrase for decryption') .option('-s, --secret <secret>', 'Secret key from environment variable') .option('-c, --cwd <path>', 'Working directory path') .option('-a, --all', 'Process all directories with environment files') .option('--overwrite', 'Overwrite existing .env file without confirmation') .action(async (options) => { try { await executeCopy(options); } catch (error) { exec_1.CliUtils.error(`Copy failed: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }); return command; }; exports.createCopyCommand = createCopyCommand; async function executeCopy(rawOptions) { exec_1.CliUtils.header('Environment File Copy'); if (rawOptions.all) { if (!rawOptions.environment) { throw new Error('The --all flag requires an environment to be specified with -e/--environment'); } } const cwd = rawOptions.cwd || exec_1.ExecUtils.getCurrentDir(); const availableEnvironments = await file_1.FileUtils.findAllEnvironments(cwd); if (availableEnvironments.length === 0) { exec_1.CliUtils.warning('No environment files found in the current directory.'); exec_1.CliUtils.info('Use the "create" command to create environment files first.'); return; } if (rawOptions.all) { await processAllDirectories(rawOptions, rawOptions.environment, cwd); return; } let environment = rawOptions.environment || ''; if (!environment) { if (availableEnvironments.length === 1) { environment = availableEnvironments[0]; exec_1.CliUtils.info(`Using environment: ${exec_1.CliUtils.formatEnvironment(environment)}`); } else { environment = await interactive_1.InteractiveUtils.selectEnvironment(availableEnvironments, 'Select environment to copy to .env:'); } } if (!availableEnvironments.includes(environment)) { throw new Error(`Environment '${environment}' not found. Available: ${availableEnvironments.join(', ')}`); } const currentDirEnvFiles = await file_1.FileUtils.findEnvFiles(environment, cwd); const currentDirFiles = currentDirEnvFiles.filter(file => path_1.default.dirname(file.path) === cwd && file.exists); if (currentDirFiles.length === 0 && availableEnvironments.includes(environment)) { const allEnvFiles = await file_1.FileUtils.findEnvFiles(environment, cwd); const existingFiles = allEnvFiles.filter(file => file.exists); if (existingFiles.length > 0) { exec_1.CliUtils.warning(`No ${exec_1.CliUtils.formatEnvironment(environment)} files found in current directory.`); exec_1.CliUtils.info(`Found ${exec_1.CliUtils.formatEnvironment(environment)} files in other directories:`); const dirMap = new Map(); existingFiles.forEach(file => { const dir = path_1.default.dirname(file.path); const relativePath = file_1.FileUtils.getRelativePath(dir, cwd); const displayDir = relativePath === '' ? '.' : relativePath; dirMap.set(displayDir, (dirMap.get(displayDir) || 0) + 1); }); Array.from(dirMap.entries()).forEach(([dir, count]) => { console.log(` • ${exec_1.CliUtils.formatPath(dir, cwd)} (${count} file${count > 1 ? 's' : ''})`); }); const processAll = await interactive_1.InteractiveUtils.confirmOperation('Do you want to process all directories with this environment?', false); if (processAll) { await processAllDirectories(rawOptions, environment, cwd); return; } else { exec_1.CliUtils.info('Operation cancelled.'); return; } } else { exec_1.CliUtils.error(`No ${exec_1.CliUtils.formatEnvironment(environment)} files found in the project.`); return; } } await processSingleEnvironmentInSourceDirectory(rawOptions, environment, cwd); } async function processSingleEnvironmentInSourceDirectory(rawOptions, environment, cwd) { const envFiles = await file_1.FileUtils.findEnvFiles(environment, cwd); const currentDirFiles = envFiles.filter(file => path_1.default.dirname(file.path) === cwd && file.exists); if (currentDirFiles.length === 0) { exec_1.CliUtils.error(`No ${exec_1.CliUtils.formatEnvironment(environment)} files found in current directory.`); return; } const result = await processSingleEnvironment({ ...rawOptions, cwd }, environment, cwd, false); if (!result.success) { process.exit(1); } } async function processSingleEnvironment(rawOptions, environment, workingDir, isPartOfAll = false) { const rootCwd = rawOptions.cwd || process.cwd(); const envFiles = await file_1.FileUtils.findEnvFiles(environment, rootCwd); const sourceFiles = envFiles.filter(file => file.exists && path_1.default.dirname(file.path) === workingDir); if (sourceFiles.length === 0) { const message = `No files found for environment '${environment}'`; if (!isPartOfAll) { exec_1.CliUtils.error(message); } return { success: false, error: message }; } let sourceFile = sourceFiles.find(file => !file.encrypted); let isEncrypted = false; if (!sourceFile) { sourceFile = sourceFiles.find(file => file.encrypted); isEncrypted = true; } if (!sourceFile) { const message = `No valid source file found for environment '${environment}'`; if (!isPartOfAll) { exec_1.CliUtils.error(message); } return { success: false, error: message }; } const sourcePath = isEncrypted ? file_1.FileUtils.getEncryptedPath(sourceFile.path) : sourceFile.path; const targetPath = path_1.default.join(workingDir, '.env'); if (!isPartOfAll) { const rootCwd = rawOptions.cwd || process.cwd(); exec_1.CliUtils.info(`Source: ${exec_1.CliUtils.formatPath(file_1.FileUtils.getRelativePath(sourcePath, rootCwd), rootCwd)}`); exec_1.CliUtils.info(`Target: ${exec_1.CliUtils.formatPath(file_1.FileUtils.getRelativePath(targetPath, rootCwd), rootCwd)}`); if (isEncrypted) { exec_1.CliUtils.info(`File is encrypted - will decrypt during copy`); } } else { const rootCwd = rawOptions.cwd || process.cwd(); const targetDir = file_1.FileUtils.getRelativePath(path_1.default.dirname(targetPath), rootCwd); const displayTargetDir = targetDir === '' ? '.' : targetDir; exec_1.CliUtils.info(`Copying ${isEncrypted ? 'encrypted' : ''} ${file_1.FileUtils.getRelativePath(sourcePath, rootCwd)}${displayTargetDir}/.env`); } if (!(await file_1.FileUtils.fileExists(sourcePath))) { const message = `Source file not found: ${sourcePath}`; if (!isPartOfAll) { exec_1.CliUtils.error(message); } return { success: false, error: message }; } const targetExists = await file_1.FileUtils.fileExists(targetPath); if (targetExists && !rawOptions.overwrite && !isPartOfAll) { exec_1.CliUtils.warning('Target .env file already exists'); try { const currentSize = (await file_1.FileUtils.getFileStats(targetPath)).size; console.log(`Current .env file size: ${currentSize} bytes`); } catch { } const confirm = await interactive_1.InteractiveUtils.confirmOperation('Do you want to overwrite the existing .env file?', false); if (!confirm) { exec_1.CliUtils.info('Operation cancelled.'); return { success: false, error: 'Operation cancelled by user' }; } } if (isEncrypted) { if (!exec_1.ExecUtils.isGpgAvailable()) { const message = 'GPG is not available. Please install GPG to decrypt files.'; if (!isPartOfAll) { exec_1.CliUtils.error(message); interactive_1.InteractiveUtils.displayPrerequisites(); process.exit(1); } else { throw new Error(message); } } let passphrase = rawOptions.passphrase || ''; if (!passphrase || passphrase.trim() === '') { const rootCwd = rawOptions.cwd || process.cwd(); const envrcConfig = await file_1.FileUtils.readEnvrc(rootCwd); const secretVar = file_1.FileUtils.generateSecretVariableName(environment); if (rawOptions.secret && envrcConfig[rawOptions.secret]) { passphrase = envrcConfig[rawOptions.secret]; exec_1.CliUtils.info(`Using secret from .envrc: ${chalk_1.default.cyan(rawOptions.secret)}`); } else if (envrcConfig[secretVar]) { passphrase = envrcConfig[secretVar]; exec_1.CliUtils.info(`Using secret from .envrc: ${chalk_1.default.cyan(secretVar)}`); } else { passphrase = await interactive_1.InteractiveUtils.promptPassphrase(`Enter decryption passphrase for ${exec_1.CliUtils.formatEnvironment(environment)}:`); } } (0, schemas_1.validateCopyOptions)({ environment, passphrase, cwd: workingDir, overwrite: rawOptions.overwrite, }); if (!isPartOfAll) { exec_1.CliUtils.info('Testing GPG operation...'); } const gpgTest = exec_1.ExecUtils.testGpgOperation(passphrase); if (!gpgTest.success) { throw new Error(`GPG test failed: ${gpgTest.message}`); } if (!isPartOfAll) { exec_1.CliUtils.success('GPG test passed'); } let backupPath = null; if (targetExists) { backupPath = await file_1.FileUtils.createBackup(targetPath); if (!isPartOfAll) { const rootCwd = rawOptions.cwd || process.cwd(); exec_1.CliUtils.info(`Created backup: ${chalk_1.default.gray(file_1.FileUtils.getRelativePath(backupPath, rootCwd))}`); } } if (!isPartOfAll) { exec_1.CliUtils.info('Decrypting and copying...'); } try { const decryptResult = exec_1.ExecUtils.decryptFile(sourcePath, targetPath, passphrase); if (decryptResult.success) { exec_1.CliUtils.success(`Successfully copied and decrypted to .env`); if (backupPath) { await file_1.FileUtils.removeBackup(backupPath); } } else { if (backupPath) { exec_1.ExecUtils.moveFile(backupPath, targetPath); exec_1.CliUtils.info('Restored original .env file from backup'); } exec_1.CliUtils.error(`Failed to decrypt: ${decryptResult.message}`); if (decryptResult.errors) { decryptResult.errors.forEach(error => { console.log(chalk_1.default.red(` • ${error}`)); }); } if (!isPartOfAll) { process.exit(1); } else { return { success: false, error: decryptResult.message }; } } } catch (error) { if (backupPath) { try { exec_1.ExecUtils.moveFile(backupPath, targetPath); exec_1.CliUtils.info('Restored original .env file from backup'); } catch { } } throw error; } } else { (0, schemas_1.validateCopyOptions)({ environment, cwd: workingDir, overwrite: rawOptions.overwrite, }); if (!isPartOfAll) { exec_1.CliUtils.info('Copying file...'); } try { const copyResult = exec_1.ExecUtils.copyFile(sourcePath, targetPath); if (copyResult.success) { exec_1.CliUtils.success(`Successfully copied to .env`); } else { exec_1.CliUtils.error(`Failed to copy: ${copyResult.message}`); if (copyResult.errors) { copyResult.errors.forEach(error => { console.log(chalk_1.default.red(` • ${error}`)); }); } if (!isPartOfAll) { process.exit(1); } else { return { success: false, error: copyResult.message }; } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); exec_1.CliUtils.error(`Error copying file: ${errorMessage}`); if (!isPartOfAll) { process.exit(1); } else { return { success: false, error: errorMessage }; } } } if (!isPartOfAll) { console.log(); exec_1.CliUtils.subheader('Copy Summary'); try { const targetStats = await file_1.FileUtils.getFileStats(targetPath); const rootCwd = rawOptions.cwd || process.cwd(); const targetDir = file_1.FileUtils.getRelativePath(path_1.default.dirname(targetPath), rootCwd); const displayTargetDir = targetDir === '' ? '.' : targetDir; console.log(`Target file: ${exec_1.CliUtils.formatPath(path_1.default.join(displayTargetDir, '.env'), rootCwd)} (${targetStats.size} bytes)`); } catch { const rootCwd = rawOptions.cwd || process.cwd(); const targetDir = file_1.FileUtils.getRelativePath(path_1.default.dirname(targetPath), rootCwd); const displayTargetDir = targetDir === '' ? '.' : targetDir; console.log(`Target file: ${exec_1.CliUtils.formatPath(path_1.default.join(displayTargetDir, '.env'), rootCwd)}`); } console.log(); exec_1.CliUtils.info('Next steps:'); console.log(chalk_1.default.gray('• Review the .env file contents')); console.log(chalk_1.default.gray('• Your application can now use the .env file')); console.log(chalk_1.default.gray('• Remember: .env files should be added to .gitignore')); if (['production', 'prod'].includes(environment.toLowerCase())) { console.log(); exec_1.CliUtils.warning('Security Notice:'); console.log(chalk_1.default.yellow('You are using production environment variables locally.')); console.log(chalk_1.default.yellow('Ensure this .env file is not committed to version control.')); } } return { success: true }; } async function processAllDirectories(rawOptions, environment, cwd) { exec_1.CliUtils.info(`Processing all directories for environment: ${exec_1.CliUtils.formatEnvironment(environment)}`); const allEnvFiles = await file_1.FileUtils.findEnvFiles(environment, cwd); const existingFiles = allEnvFiles.filter(file => file.exists); if (existingFiles.length === 0) { exec_1.CliUtils.warning(`No ${exec_1.CliUtils.formatEnvironment(environment)} files found in the project.`); return; } const filesByDirectory = new Map(); for (const file of existingFiles) { const dir = path_1.default.dirname(file.path); if (!filesByDirectory.has(dir)) { filesByDirectory.set(dir, []); } filesByDirectory.get(dir).push(file); } const directories = Array.from(filesByDirectory.keys()).sort(); exec_1.CliUtils.info(`Found ${exec_1.CliUtils.formatEnvironment(environment)} files in ${directories.length} director${directories.length === 1 ? 'y' : 'ies'}:`); directories.forEach(dir => { const relativePath = file_1.FileUtils.getRelativePath(dir, cwd); const displayDir = relativePath === '' ? '.' : relativePath; console.log(` • ${exec_1.CliUtils.formatPath(displayDir, cwd)}`); }); let totalSuccess = 0; let totalErrors = 0; const results = []; for (const directory of directories) { console.log(); const relativeDir = file_1.FileUtils.getRelativePath(directory, cwd); const displayDir = relativeDir === '' ? '.' : relativeDir; exec_1.CliUtils.subheader(`Processing Directory: ${exec_1.CliUtils.formatPath(displayDir, cwd)}`); try { const result = await processSingleEnvironment({ ...rawOptions, cwd }, environment, directory, true); if (result.success) { results.push({ directory: displayDir, success: true }); totalSuccess++; } else { results.push({ directory: displayDir, success: false, error: result.error || 'Unknown error', }); totalErrors++; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); exec_1.CliUtils.error(`Failed to process directory '${displayDir}': ${errorMessage}`); results.push({ directory: displayDir, success: false, error: errorMessage, }); totalErrors++; } } console.log(); exec_1.CliUtils.header('Overall Summary'); results.forEach(result => { if (result.success) { console.log(`${exec_1.CliUtils.formatPath(result.directory, cwd)}: ${chalk_1.default.green('✓ Success')}`); } else { console.log(`${exec_1.CliUtils.formatPath(result.directory, cwd)}: ${chalk_1.default.red('✗ Failed')} - ${result.error}`); } }); console.log(); if (totalSuccess > 0) { exec_1.CliUtils.success(`Total: Successfully copied ${exec_1.CliUtils.formatEnvironment(environment)} to .env in ${totalSuccess} director${totalSuccess === 1 ? 'y' : 'ies'}`); } if (totalErrors > 0) { exec_1.CliUtils.error(`Total: Failed to copy in ${totalErrors} director${totalErrors === 1 ? 'y' : 'ies'}`); } if (totalSuccess > 0) { console.log(); exec_1.CliUtils.info('Next steps:'); console.log(chalk_1.default.gray('• Review the .env files in each directory')); console.log(chalk_1.default.gray('• Your applications can now use the .env files')); console.log(chalk_1.default.gray('• Remember: .env files should be added to .gitignore')); if (['production', 'prod'].includes(environment.toLowerCase())) { console.log(); exec_1.CliUtils.warning('Security Notice:'); console.log(chalk_1.default.yellow('You are using production environment variables across multiple services.')); console.log(chalk_1.default.yellow('Ensure .env files are not committed to version control.')); } } if (totalErrors > 0) { process.exit(1); } } //# sourceMappingURL=copy.js.map