UNPKG

dotenvx-interactive-cli

Version:

An interactive CLI tool for managing .env files with dotenvx - encrypt, decrypt, and manage environment variables with ease.

233 lines (230 loc) 7.86 kB
#!/usr/bin/env node import { Args, Command } from '@effect/cli'; import { NodeContext, NodeRuntime } from '@effect/platform-node'; import { Effect, Console, Option } from 'effect'; import { spawnSync, spawn } from 'child_process'; import { createRequire } from 'module'; import { promises } from 'fs'; import { fileURLToPath } from 'url'; import { checkbox } from '@inquirer/prompts'; import { findEnvFiles } from './findEnvFiles.js'; const require2 = createRequire(import.meta.url); const pkg = require2("../package.json"); const VERSION = pkg.version; const executeCommand = (cmd, ...args) => Effect.async((resume) => { const child = spawn(cmd, args); let stdout = ""; let stderr = ""; child.stdout?.on("data", (data) => { process.stdout.write(data); stdout += data.toString(); }); child.stderr?.on("data", (data) => { process.stderr.write(data); stderr += data.toString(); }); child.on("close", (code) => { resume(Effect.succeed({ stdout, stderr, exitCode: code ?? 0 })); }); child.on("error", (error) => { resume(Effect.fail(error)); }); }); const checkDotenvxInstallation = () => Effect.sync(() => { try { const cmd = process.platform === "win32" ? "where" : "which"; const result = spawnSync(cmd, ["dotenvx"], { encoding: "utf8" }); return result.status === 0; } catch { return false; } }); const checkEnvKeysFile = () => Effect.tryPromise({ try: () => promises.access(".env.keys"), catch: () => new Error("File not found") }).pipe( Effect.map(() => true), Effect.catchAll(() => Effect.succeed(false)) ); const selectFilesInteractively = (files2, action) => Effect.tryPromise({ try: async () => { if (files2.length === 0) { return []; } const choices = [ { name: `\u{1F5C2}\uFE0F All files (${files2.length} files)`, value: "ALL", short: "All files" }, ...files2.map((file) => ({ name: `\u{1F4C4} ${file}`, value: file, short: file })) ]; const selectedFiles = await checkbox({ message: `Select .env files to ${action}:`, choices }); if (selectedFiles.includes("ALL")) { return files2; } return selectedFiles; }, catch: (error) => { if (error instanceof Error && error.name === "ExitPromptError") { return new Error("User cancelled selection"); } return new Error(`Interactive selection failed: ${error}`); } }); const validatePrerequisites = Effect.gen(function* () { yield* Console.log("dotenvx-interactive-cli"); const [isDotenvxInstalled, isEnvKeysFilePresent] = yield* Effect.all([ checkDotenvxInstallation(), checkEnvKeysFile() ]); if (!isDotenvxInstalled) { yield* Console.log("\u274C dotenvx is not installed on your system."); yield* Console.log("Please install it using: npm install -g @dotenvx/dotenvx"); return yield* Effect.fail(new Error("dotenvx not installed")); } if (!isEnvKeysFilePresent) { yield* Console.log("No .env.keys file found. Please create one to proceed."); return yield* Effect.fail(new Error(".env.keys file not found")); } yield* Console.log("\u{1F44C} dotenvx is installed"); return true; }); const files = Args.text({ name: "files" }).pipe(Args.repeated, Args.optional); const encryptCommand = Command.make( "encrypt", { files }, ({ files: files2 }) => Effect.gen(function* () { yield* validatePrerequisites; const envFiles = yield* findEnvFiles(); if (envFiles.length === 0) { yield* Console.log("No .env files found in the current directory."); return; } let filesToEncrypt; if (Option.isSome(files2) && files2.value.length > 0) { filesToEncrypt = files2.value; } else { yield* Console.log(""); const result2 = yield* Effect.either(selectFilesInteractively(envFiles, "encrypt")); if (result2._tag === "Left") { if (result2.left.message === "User cancelled selection") { yield* Console.log("\u{1F44B} Selection cancelled. Until next time!"); return; } else { return yield* Effect.fail(result2.left); } } filesToEncrypt = result2.right; if (filesToEncrypt.length === 0) { yield* Console.log("No files selected for encryption"); return; } } yield* Console.log(`Encrypting files: ${filesToEncrypt.join(", ")}`); const result = yield* executeCommand("dotenvx", "encrypt", "-f", ...filesToEncrypt); if (result.exitCode === 0) { yield* Console.log("\u2713 Files encrypted successfully"); } else { yield* Console.error(`\u274C Failed to encrypt files: ${result.stderr}`); return yield* Effect.fail(new Error("Encryption failed")); } }) ); const decryptCommand = Command.make( "decrypt", { files }, ({ files: files2 }) => Effect.gen(function* () { yield* validatePrerequisites; const envFiles = yield* findEnvFiles(); if (envFiles.length === 0) { yield* Console.log("No .env files found in the current directory."); return; } let filesToDecrypt; if (Option.isSome(files2) && files2.value.length > 0) { filesToDecrypt = files2.value; } else { yield* Console.log(""); const result2 = yield* Effect.either(selectFilesInteractively(envFiles, "decrypt")); if (result2._tag === "Left") { if (result2.left.message === "User cancelled selection") { yield* Console.log("\u{1F44B} Selection cancelled. Until next time!"); return; } else { return yield* Effect.fail(result2.left); } } filesToDecrypt = result2.right; if (filesToDecrypt.length === 0) { yield* Console.log("No files selected for decryption"); return; } } yield* Console.log(`Decrypting files: ${filesToDecrypt.join(", ")}`); const result = yield* executeCommand("dotenvx", "decrypt", "-f", ...filesToDecrypt); if (result.exitCode === 0) { yield* Console.log("\u2713 Files decrypted successfully"); } else { yield* Console.error(`\u274C Failed to decrypt files: ${result.stderr}`); return yield* Effect.fail(new Error("Decryption failed")); } }) ); const precommitCommand = Command.make( "precommit", {}, () => Effect.gen(function* () { yield* validatePrerequisites; yield* Console.log("Installing precommit hook..."); const result = yield* executeCommand("dotenvx", "ext", "precommit", "--install"); if (result.exitCode === 0) { yield* Console.log("\u{1F44D} Precommit hook installed successfully"); } else { yield* Console.error(`\u274C Failed to install precommit hook: ${result.stderr}`); return yield* Effect.fail(new Error("Precommit installation failed")); } }) ); const mainCommand = Command.make( "dotenvx-interactive-cli", {}, () => Effect.gen(function* () { yield* validatePrerequisites; yield* Console.log(""); yield* Console.log("Available commands:"); yield* Console.log(" encrypt - Encrypt .env files"); yield* Console.log(" decrypt - Decrypt .env files"); yield* Console.log(" precommit - Install precommit hook"); yield* Console.log(""); yield* Console.log("Use --help with any command for more information."); }) ); const cli = mainCommand.pipe( Command.withSubcommands([encryptCommand, decryptCommand, precommitCommand]) ); const app = Command.run(cli, { name: "dotenvx-interactive-cli", version: VERSION }); const isMain = process.argv[1] === fileURLToPath(import.meta.url); if (isMain) { Effect.suspend(() => app(process.argv)).pipe( Effect.provide(NodeContext.layer), Effect.catchAll( (error) => Console.error(`\u274C An error occurred: ${error}`) ) ).pipe(NodeRuntime.runMain); } export { VERSION };