dotkit
Version:
A powerful CLI toolkit for managing environment variables and .env files.
209 lines (203 loc) • 9.82 kB
JavaScript
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