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
JavaScript
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 };