envx-cli
Version:
Environment file encryption and management tool
266 lines • 10.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileUtils = void 0;
const crypto_1 = __importDefault(require("crypto"));
const fast_glob_1 = __importDefault(require("fast-glob"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const lodash_1 = require("lodash");
const path_1 = __importDefault(require("path"));
class FileUtils {
static async findEnvFiles(environment, cwd) {
const pattern = `**/.env.${environment}`;
const encryptedPattern = `**/.env.${environment}.gpg`;
const [envFiles, encryptedFiles] = await Promise.all([
(0, fast_glob_1.default)(pattern, { cwd, dot: true }),
(0, fast_glob_1.default)(encryptedPattern, { cwd, dot: true }),
]);
const results = [];
for (const filePath of envFiles) {
results.push({
path: path_1.default.join(cwd, filePath),
stage: environment,
encrypted: false,
exists: true,
});
}
for (const filePath of encryptedFiles) {
const decryptedPath = (0, lodash_1.replace)(filePath, '.gpg', '');
results.push({
path: path_1.default.join(cwd, decryptedPath),
stage: environment,
encrypted: true,
exists: true,
});
}
return results;
}
static async findAllEnvironments(cwd) {
const pattern = '**/.env.*';
const files = await (0, fast_glob_1.default)(pattern, { cwd, dot: true });
const environments = new Set();
for (const file of files) {
const basename = path_1.default.basename(file);
const match = basename.match(/^\.env\.([^.]+)(\.gpg)?$/);
if (match && match[1]) {
environments.add(match[1]);
}
}
return Array.from(environments).sort();
}
static async fileExists(filePath) {
try {
const stat = await fs_extra_1.default.stat(filePath);
return stat.isFile();
}
catch {
return false;
}
}
static async createBackup(filePath) {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
const backupPath = `${filePath}.backup.${timestamp}.${random}`;
await fs_extra_1.default.copy(filePath, backupPath);
return backupPath;
}
static async removeBackup(backupPath) {
if (await this.fileExists(backupPath)) {
await fs_extra_1.default.remove(backupPath);
}
}
static async getFileHash(filePath) {
const fileBuffer = await fs_extra_1.default.readFile(filePath);
return crypto_1.default.createHash('md5').update(fileBuffer).digest('hex');
}
static async filesAreIdentical(file1, file2) {
if (!(await this.fileExists(file1)) || !(await this.fileExists(file2))) {
return false;
}
const [hash1, hash2] = await Promise.all([
this.getFileHash(file1),
this.getFileHash(file2),
]);
return hash1 === hash2;
}
static async readEnvrc(cwd) {
const envrcPath = path_1.default.join(cwd, '.envrc');
if (!(await this.fileExists(envrcPath))) {
return {};
}
try {
const content = await fs_extra_1.default.readFile(envrcPath, 'utf-8');
const config = {};
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('export ')) {
const exportStatement = trimmed.substring(7);
const [key, ...valueParts] = exportStatement.split('=');
if (key && valueParts.length > 0) {
let value = valueParts.join('=');
value = value.replace(/^["']|["']$/g, '');
config[key.trim()] = value;
}
}
}
return config;
}
catch (error) {
console.warn(`Warning: Could not read .envrc file: ${error}`);
return {};
}
}
static async writeEnvrc(cwd, config) {
const envrcPath = path_1.default.join(cwd, '.envrc');
try {
const lines = ['# Environment secrets generated by envx', ''];
for (const [key, value] of Object.entries(config)) {
lines.push(`export ${key}="${value}"`);
}
lines.push('');
await fs_extra_1.default.writeFile(envrcPath, lines.join('\n'), 'utf-8');
return {
success: true,
message: 'Successfully wrote .envrc file',
filePath: envrcPath,
};
}
catch (error) {
return {
success: false,
message: `Failed to write .envrc file: ${error}`,
filePath: envrcPath,
error: error,
};
}
}
static async createEnvTemplate(filePath, template) {
try {
let content = '';
if (template && (await this.fileExists(template))) {
content = await fs_extra_1.default.readFile(template, 'utf-8');
}
else {
content = [
'# Environment variables',
'# Add your environment-specific variables here',
'',
'# Example:',
'# DATABASE_URL=',
'# API_KEY=',
'# DEBUG=false',
'',
].join('\n');
}
await fs_extra_1.default.writeFile(filePath, content, 'utf-8');
return {
success: true,
message: `Successfully created .env file`,
filePath,
};
}
catch (error) {
return {
success: false,
message: `Failed to create .env file: ${error}`,
filePath,
error: error,
};
}
}
static generateRandomSecret(length = 32) {
return crypto_1.default.randomBytes(length).toString('hex');
}
static generateSecretVariableName(stage) {
return `${stage.toUpperCase()}_SECRET`;
}
static async ensureDir(dirPath) {
await fs_extra_1.default.ensureDir(dirPath);
}
static getRelativePath(absolutePath, cwd) {
return path_1.default.relative(cwd, absolutePath);
}
static isValidEnvironmentName(name) {
return /^[a-zA-Z0-9_-]+$/.test(name);
}
static getEncryptedPath(filePath) {
return `${filePath}.gpg`;
}
static getDecryptedPath(encryptedPath) {
return (0, lodash_1.replace)(encryptedPath, '.gpg', '');
}
static isEncryptedFile(filePath) {
return filePath.endsWith('.gpg');
}
static async getFileStats(filePath) {
const stats = await fs_extra_1.default.stat(filePath);
return {
size: stats.size,
mtime: stats.mtime,
};
}
static async updateGitignore(cwd) {
const gitignorePath = path_1.default.join(cwd, '.gitignore');
const envPatterns = ['.env.*', '!.env.example', '!.env.*.gpg'];
const secretPatterns = ['.envrc'];
try {
let existingContent = '';
if (await this.fileExists(gitignorePath)) {
existingContent = await fs_extra_1.default.readFile(gitignorePath, 'utf-8');
}
const missingEnvPatterns = envPatterns.filter(pattern => !existingContent.includes(pattern));
const missingSecretPatterns = secretPatterns.filter(pattern => !existingContent.includes(pattern));
if (missingEnvPatterns.length === 0 &&
missingSecretPatterns.length === 0) {
return {
success: true,
message: '.gitignore already contains all EnvX patterns',
filePath: gitignorePath,
};
}
let newContent = existingContent.trim();
const addedSections = [];
if (missingEnvPatterns.length > 0) {
const envSection = [
'',
'# Environment files',
...missingEnvPatterns.map(pattern => pattern === '!.env.*.gpg' ? `${pattern}` : pattern),
];
newContent += (newContent ? '\n' : '') + envSection.join('\n');
addedSections.push('Environment files');
}
if (missingSecretPatterns.length > 0) {
const secretSection = [
'',
'# EnvX secrets',
...missingSecretPatterns.map(pattern => pattern === '.envrc' ? `${pattern}` : pattern),
];
newContent += (newContent ? '\n' : '') + secretSection.join('\n');
addedSections.push('EnvX secrets');
}
newContent += '\n';
await fs_extra_1.default.writeFile(gitignorePath, newContent, 'utf-8');
const message = addedSections.length > 0
? `Successfully updated .gitignore with ${addedSections.join(' and ')} patterns`
: 'Successfully updated .gitignore with EnvX patterns';
return {
success: true,
message,
filePath: gitignorePath,
};
}
catch (error) {
return {
success: false,
message: `Failed to update .gitignore: ${error}`,
filePath: gitignorePath,
error: error,
};
}
}
}
exports.FileUtils = FileUtils;
//# sourceMappingURL=file.js.map