UNPKG

dotkit

Version:

A powerful CLI toolkit for managing environment variables and .env files.

209 lines (203 loc) 9.82 kB
#!/usr/bin/env node import process from "node:process"; import { program } from "commander"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { parse } from "dotenv"; import { randomBytes } from "node:crypto"; //#region package.json var version = "1.4.0"; //#endregion //#region src/lib/common.ts function generateRandomHex(length = 32) { return randomBytes(length).toString("hex"); } function getValueForKey(key, templateParsed) { return templateParsed[key] || ""; } //#endregion //#region src/lib/secret.ts function generateVariables(options) { const { envPath, variables, length = 32, dryRun, force } = options; const bootstrapped = !existsSync(envPath); let existingKeys = []; let variablesToGenerate = variables; if (!bootstrapped) { const existingContent = readFileSync(envPath, "utf8"); const existingParsed = parse(existingContent); existingKeys = Object.keys(existingParsed); if (!force) variablesToGenerate = variables.filter((key) => !(key in existingParsed)); } if (!dryRun && variablesToGenerate.length > 0) { const lines = variablesToGenerate.map((key) => `${key}="${generateRandomHex(length)}"`); if (bootstrapped) writeFileSync(envPath, `${lines.join("\n")}\n`); else if (force && variables.some((key) => existingKeys.includes(key))) { const existingContent = readFileSync(envPath, "utf8"); const existingParsed = parse(existingContent); const variablesToRemove = variables.filter((key) => key in existingParsed); let updatedContent = existingContent; variablesToRemove.forEach((key) => { const regex = new RegExp(`^${key}=.*$`, "gm"); updatedContent = updatedContent.replace(regex, ""); }); updatedContent = updatedContent.replace(/\n\n+/g, "\n").trim(); const newContent = `${updatedContent + (updatedContent ? "\n\n" : "") + lines.join("\n")}\n`; writeFileSync(envPath, newContent); } else writeFileSync(envPath, `\n${lines.join("\n")}\n`, { flag: "a" }); } const missingKeyValues = variablesToGenerate.reduce((acc, key) => { acc[key] = generateRandomHex(length); return acc; }, {}); return { bootstrapped, missingCount: variablesToGenerate.length, missingKeys: variablesToGenerate, missingKeyValues }; } //#endregion //#region src/lib/sync.ts function getKeysToProcess(templateParsed, variables, skipEmptySourceValues) { const allTemplateKeys = Object.keys(templateParsed); let keysToInclude = variables && variables.length > 0 ? variables.filter((key) => allTemplateKeys.includes(key)) : allTemplateKeys; if (skipEmptySourceValues) keysToInclude = keysToInclude.filter((key) => templateParsed[key] && templateParsed[key].trim() !== ""); return keysToInclude; } function bootstrapEnvFile(envPath, templateContent, templateParsed, keysToBootstrap, variables, dryRun, skipEmptySourceValues) { if (dryRun) return; if (variables && variables.length > 0 || skipEmptySourceValues) { const filteredLines = keysToBootstrap.map((key) => `${key}="${getValueForKey(key, templateParsed)}"`); writeFileSync(envPath, `${filteredLines.join("\n")}\n`); return; } writeFileSync(envPath, templateContent); } function appendMissingVariables(envPath, missingKeys, defaults, dryRun) { if (dryRun || missingKeys.length === 0) return; const lines = missingKeys.map((k) => `${k}="${getValueForKey(k, defaults)}"`); writeFileSync(envPath, `\n${lines.join("\n")}\n`, { flag: "a" }); } function syncDotenv(options) { const { envPath, templatePath, variables, dryRun, overwriteEmptyValues = true, skipEmptySourceValues = false } = options; const templateContent = readFileSync(templatePath, "utf8"); const templateParsed = parse(templateContent); if (!existsSync(envPath)) { const keysToBootstrap = getKeysToProcess(templateParsed, variables, skipEmptySourceValues); bootstrapEnvFile(envPath, templateContent, templateParsed, keysToBootstrap, variables, dryRun, skipEmptySourceValues); const missingKeyValues$1 = keysToBootstrap.reduce((acc, key) => { acc[key] = getValueForKey(key, templateParsed); return acc; }, {}); return { bootstrapped: true, missingCount: keysToBootstrap.length, missingKeys: keysToBootstrap, missingKeyValues: missingKeyValues$1 }; } const current = parse(readFileSync(envPath, "utf8")); const availableKeys = getKeysToProcess(templateParsed, variables, skipEmptySourceValues); const missingKeys = availableKeys.filter((key) => { if (!(key in current)) return true; if (overwriteEmptyValues && current[key] === "" && templateParsed[key] && templateParsed[key].trim() !== "") return true; return false; }); appendMissingVariables(envPath, missingKeys, templateParsed, dryRun); const missingKeyValues = missingKeys.reduce((acc, key) => { acc[key] = getValueForKey(key, templateParsed); return acc; }, {}); return { bootstrapped: false, missingCount: missingKeys.length, missingKeys, missingKeyValues }; } //#endregion //#region src/index.ts program.name("dotkit").description("A powerful CLI toolkit for managing environment variables and dotenv files").version(version); program.command("sync").description("Sync environment variables from template to .env file").option("-t, --target <path>", "target .env file (destination)", ".env").option("-s, --source <path>", "source file to sync from (e.g., .env.example)", ".env.example").option("--only <variables...>", "only copy these specific variables").option("--dry-run", "show what would be copied without making changes").option("--no-overwrite-empty-values", "don't overwrite empty values in target file (default: overwrite)").option("--skip-empty-source-values", "skip variables with empty values in source file (default: include)").action((options) => { try { const result = syncDotenv({ envPath: options.target, templatePath: options.source, variables: options.only, dryRun: options.dryRun, overwriteEmptyValues: options.overwriteEmptyValues, skipEmptySourceValues: options.skipEmptySourceValues }); if (options.dryRun) if (result.bootstrapped) { console.log(`[DRY RUN] Would create ${options.target} from ${options.source}`); if (result.missingKeys.length > 0) { console.log(`[DRY RUN] Would copy these variables:`); result.missingKeys.forEach((key) => { const value = result.missingKeyValues?.[key] || ""; console.log(` ${key}="${value}"`); }); } } else if (result.missingCount === 0) console.log("[DRY RUN] All variables already present – nothing to do."); else { console.log(`[DRY RUN] Would append ${result.missingCount} variable(s) to ${options.target}:`); result.missingKeys.forEach((key) => { const value = result.missingKeyValues?.[key] || ""; console.log(` ${key}="${value}"`); }); } else if (result.bootstrapped) console.log(`Created ${options.target} from ${options.source}`); else if (result.missingCount === 0) console.log("All variables already present – nothing to do."); else console.log(`Appended ${result.missingCount} variable(s) to ${options.target}`); } catch (error) { console.error("Error:", error instanceof Error ? error.message : error); process.exit(1); } }); program.command("secret").alias("generate").description("Generate random hex values for environment variables").argument("<variables...>", "variable names to generate values for").option("-t, --target <path>", "target .env file", ".env").option("-l, --length <bytes>", "length in bytes for generated values", "32").option("--dry-run", "show what would be generated without making changes").option("-f, --force", "overwrite existing values").action((variables, options) => { try { const result = generateVariables({ envPath: options.target, variables, length: parseInt(options.length, 10), dryRun: options.dryRun, force: options.force }); if (options.dryRun) if (result.bootstrapped) { console.log(`[DRY RUN] Would create ${options.target} with generated values:`); result.missingKeys.forEach((key) => { const value = result.missingKeyValues?.[key] || ""; console.log(` ${key}="${value}"`); }); } else if (result.missingKeys.length === 0) { console.log(`[DRY RUN] All variables already exist in ${options.target} – nothing to do.`); if (!options.force) console.log(`[DRY RUN] Use -f or --force to overwrite existing values.`); } else if (result.missingKeys.length < variables.length) { const existing = variables.filter((v) => !result.missingKeys.includes(v)); console.log(`[DRY RUN] Would generate values for:`); result.missingKeys.forEach((key) => { const value = result.missingKeyValues?.[key] || ""; console.log(` ${key}="${value}"`); }); console.log(`[DRY RUN] Already exist (skipping): ${existing.join(", ")}`); } else { console.log(`[DRY RUN] Would generate values for:`); result.missingKeys.forEach((key) => { const value = result.missingKeyValues?.[key] || ""; console.log(` ${key}="${value}"`); }); } else if (result.bootstrapped) console.log(`Created ${options.target} with generated values for: ${variables.join(", ")}`); else if (result.missingKeys.length === 0) { console.log(`All variables already exist in ${options.target} – nothing to do.`); if (!options.force) console.log(`Use -f or --force to overwrite existing values.`); } else if (result.missingKeys.length < variables.length) { const existing = variables.filter((v) => !result.missingKeys.includes(v)); console.log(`Generated values for: ${result.missingKeys.join(", ")}`); if (!options.force) console.log(`Already exist (skipped): ${existing.join(", ")}`); } else console.log(`Generated values for: ${result.missingKeys.join(", ")}`); } catch (error) { console.error("Error:", error instanceof Error ? error.message : error); process.exit(1); } }); program.parse(); //#endregion