dotenvx-interactive-cli
Version:
An interactive CLI tool for managing .env files with dotenvx - encrypt, decrypt, and manage environment variables with ease.
249 lines (246 loc) • 6.96 kB
JavaScript
import chalk from 'chalk';
import inquirer from 'inquirer';
import { promises } from 'fs';
import { spawnSync, spawn } from 'child_process';
import { glob } from 'glob';
async function executeCommand(cmd, ...args) {
return new Promise((resolve) => {
const child = spawn(cmd, args, { shell: true });
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) => {
resolve({
stdout,
stderr,
exitCode: code ?? 0
});
});
});
}
async function checkDotenvxInstallation() {
try {
const cmd = process.platform === "win32" ? "where" : "which";
const result = spawnSync(cmd, ["dotenvx"], { encoding: "utf8" });
return result.status === 0;
} catch {
return false;
}
}
async function checkEnvKeysFile() {
try {
await promises.access(".env.keys");
return true;
} catch {
return false;
}
}
async function findEnvFiles() {
const files = await glob(".env*", {
ignore: [".env.keys", ".env.keys.json", "*.vault"],
nodir: true
});
return files.filter(
(file) => !file.endsWith(".keys") && !file.endsWith(".vault")
);
}
async function selectFiles(files, action) {
if (files.length === 0) {
console.log(
chalk.yellow("No .env files found in the current directory.")
);
return [];
}
const { selectedFiles } = await inquirer.prompt({
type: "checkbox",
name: "selectedFiles",
message: `Select .env files to ${action}:
`,
// instructions: chalk.dim("(Press Space to select, Enter to confirm)"),
instructions: `${chalk.dim("(Press ")} ${chalk.blue(
"Space"
)} ${chalk.dim("to select, ")}${chalk.blue("Enter")} ${chalk.dim(
"to confirm, "
)}${chalk.blue("Ctrl/Cmd + C")} ${chalk.dim("to exit)")} `,
choices: [
{
name: `All files ${chalk.dim("\u2190 Select all .env files")}`,
value: "ALL",
short: "All files"
},
new inquirer.Separator(),
...files.map((file) => ({
name: `${file} ${chalk.dim("\u2190 Press Space to select")}`,
value: file,
short: file
}))
],
pageSize: 10,
loop: false,
prefix: chalk.blue("?"),
validate: (answer) => {
if (answer.length === 0) {
return "Please select at least one file to proceed";
}
return true;
}
});
if (selectedFiles.includes("ALL")) {
return files;
}
return selectedFiles;
}
async function main() {
console.log(chalk.bold("dotenvx-interactive-cli"));
if (process.argv.includes("--help") || process.argv.includes("-h")) {
console.log(`
${chalk.bold("dotenvx-interactive-cli")}
Usage: dotenvx-interactive [options]
Options:
--help, -h Show this help message
`);
process.exit(0);
}
const [isDotenvxInstalled, isEnvKeysFilePresent] = await Promise.all([
checkDotenvxInstallation(),
checkEnvKeysFile()
]);
if (!isDotenvxInstalled) {
console.log(chalk.red("\u274C dotenvx is not installed on your system."));
console.log(
chalk.yellow(
"Please install it using: npm install -g @dotenvx/dotenvx"
)
);
throw new Error("dotenvx not installed");
}
if (!isEnvKeysFilePresent) {
console.log(
chalk.yellow(
"No .env.keys file found. Please create one to proceed."
)
);
throw new Error(".env.keys file not found");
}
console.log(chalk.green("\u{1F44C} dotenvx is installed"));
console.log();
const answers = await inquirer.prompt([
{
type: "list",
name: "action",
message: "What would you like to do?",
choices: [
{ name: "Encrypt .env files", value: "encrypt" },
{ name: "Decrypt .env files", value: "decrypt" },
{ name: "Install precommit hook", value: "precommit" },
{ name: "Exit", value: "exit" }
]
}
]);
console.log();
switch (answers.action) {
case "encrypt":
try {
const files = await findEnvFiles();
const selectedFiles = await selectFiles(files, "encrypt");
if (selectedFiles.length > 0) {
await executeCommand(
"dotenvx",
"encrypt",
"-f",
...selectedFiles
);
console.log(chalk.green("\u2713 Files encrypted successfully"));
} else {
console.log(
chalk.yellow("No files selected for encryption")
);
}
} catch (error) {
console.error(chalk.red("Error encrypting files:"), error);
throw error;
}
break;
case "decrypt":
try {
const files = await findEnvFiles();
const selectedFiles = await selectFiles(files, "decrypt");
if (selectedFiles.length > 0) {
const execResult = await executeCommand(
"dotenvx",
"decrypt",
"-f",
...selectedFiles
);
if (execResult.exitCode === 0) {
console.log(
chalk.green("\u2713 Files decrypted successfully")
);
} else {
console.error(
chalk.red("\u274C Failed to decrypt files:"),
execResult.stderr
);
}
} else {
console.log(
chalk.yellow("No files selected for decryption")
);
}
} catch (error) {
console.error(chalk.red("Error decrypting files:"), error);
throw error;
}
break;
case "precommit":
try {
await executeCommand(
"dotenvx",
"ext",
"precommit",
"--install"
);
console.log(
chalk.green("\u{1F44D} Precommit hook installed successfully")
);
} catch (error) {
console.error(
chalk.red("Error installing precommit hook:"),
error
);
throw error;
}
break;
case "exit":
throw new Error("User exited");
}
}
process.on("SIGINT", () => {
console.log("\n\u{1F44B} Exiting gracefully. Until next time!");
process.exit(0);
});
process.on("uncaughtException", (error) => {
if (error instanceof Error && error.name === "ExitPromptError" || typeof error === "object" && error && error.isTtyError) {
console.log("\u{1F44B} Prompt exited. Until next time!");
process.exit(0);
} else {
console.error(chalk.red("Unexpected error:"), error);
process.exit(1);
}
});
main().catch((err) => {
if (err instanceof Error && err.name === "ExitPromptError" || typeof err === "object" && err && err.isTtyError) {
console.log("\u{1F44B} Prompt exited. Until next time!");
process.exit(0);
}
console.error(chalk.red("\u274C An error occurred:"), err);
process.exit(1);
});