envx-cli
Version:
Environment file encryption and management tool
435 lines • 20.3 kB
JavaScript
;
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