import { Check, Clipboard, FileDown, FileInput, Languages, Plus, RefreshCcw, Settings, ShieldCheck, SunMoon, Trash2, X } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import type { ReactNode } from "react"; import packageInfo from "../package.json"; import { transformEnv } from "./env"; import type { EnvDefault } from "./env"; type ThemeMode = "system" | "light" | "dark"; type Language = "de" | "en" | "es" | "fr" | "nl"; type DefaultSource = "auto" | "manual"; interface DefaultRow extends EnvDefault { source: DefaultSource; index?: number; } const initialDefaults: EnvDefault[] = []; const languageNames: Record = { de: "Deutsch", en: "English", es: "Español", fr: "Français", nl: "Nederlands" }; const translations = { de: { appSubtitle: "Lokaler Helfer für .env Vorlagen", loadFile: "Datei laden", saveFile: ".env speichern", settings: "Einstellungen", input: "Input", output: "Output", inputHint: "Datei laden oder Text direkt bearbeiten", outputHint: "Generierte .env, bereit zum Kopieren", copy: "Kopieren", copied: "Kopiert", regenerate: "Neu erzeugen", hits: "Fundstellen", values: "Werte", defaultsApplied: "Defaults", source: "Quelle", file: "Datei", text: "Text", defaults: "Standardwerte", defaultsHint: "Mögliche Standardwerte werden erkannt; eigene Werte überschreiben passende Keys im Output.", addDefault: "Standardwert hinzufügen", autoDefault: "Auto", manualDefault: "Manuell", key: "Key", value: "Wert", detected: "Erkannte Anforderungen", detectedHint: "Placeholder, betroffene Keys und erzeugtes Format.", placeholder: "Placeholder", keys: "Keys", format: "Format", noPlaceholders: "Keine CHANGE_ME Platzhalter gefunden.", language: "Sprache", theme: "Darstellung", system: "System", light: "Hell", dark: "Dunkel", version: "Version", author: "Autor", repository: "Repository" }, en: { appSubtitle: "Local helper for .env templates", loadFile: "Load file", saveFile: "Save .env", settings: "Settings", input: "Input", output: "Output", inputHint: "Load a file or edit text directly", outputHint: "Generated .env, ready to copy", copy: "Copy", copied: "Copied", regenerate: "Regenerate", hits: "Matches", values: "Values", defaultsApplied: "Defaults", source: "Source", file: "File", text: "Text", defaults: "Defaults", defaultsHint: "Possible defaults are detected; custom values override matching keys in the output.", addDefault: "Add default", autoDefault: "Auto", manualDefault: "Manual", key: "Key", value: "Value", detected: "Detected requirements", detectedHint: "Placeholder, affected keys, and generated format.", placeholder: "Placeholder", keys: "Keys", format: "Format", noPlaceholders: "No CHANGE_ME placeholders found.", language: "Language", theme: "Appearance", system: "System", light: "Light", dark: "Dark", version: "Version", author: "Author", repository: "Repository" }, es: { appSubtitle: "Ayudante local para plantillas .env", loadFile: "Cargar archivo", saveFile: "Guardar .env", settings: "Ajustes", input: "Entrada", output: "Salida", inputHint: "Carga un archivo o edita el texto", outputHint: ".env generado, listo para copiar", copy: "Copiar", copied: "Copiado", regenerate: "Regenerar", hits: "Coincidencias", values: "Valores", defaultsApplied: "Defaults", source: "Origen", file: "Archivo", text: "Texto", defaults: "Valores predeterminados", defaultsHint: "Se detectan valores posibles; los personalizados sobrescriben claves iguales.", addDefault: "Añadir valor", autoDefault: "Auto", manualDefault: "Manual", key: "Clave", value: "Valor", detected: "Requisitos detectados", detectedHint: "Placeholder, claves afectadas y formato generado.", placeholder: "Placeholder", keys: "Claves", format: "Formato", noPlaceholders: "No se encontraron placeholders CHANGE_ME.", language: "Idioma", theme: "Apariencia", system: "Sistema", light: "Claro", dark: "Oscuro", version: "Versión", author: "Autor", repository: "Repositorio" }, fr: { appSubtitle: "Assistant local pour modèles .env", loadFile: "Charger", saveFile: "Enregistrer .env", settings: "Réglages", input: "Entrée", output: "Sortie", inputHint: "Chargez un fichier ou modifiez le texte", outputHint: ".env généré, prêt à copier", copy: "Copier", copied: "Copié", regenerate: "Régénérer", hits: "Occurrences", values: "Valeurs", defaultsApplied: "Défauts", source: "Source", file: "Fichier", text: "Texte", defaults: "Valeurs par défaut", defaultsHint: "Les valeurs possibles sont détectées; les valeurs personnalisées remplacent les clés.", addDefault: "Ajouter", autoDefault: "Auto", manualDefault: "Manuel", key: "Clé", value: "Valeur", detected: "Exigences détectées", detectedHint: "Placeholder, clés concernées et format généré.", placeholder: "Placeholder", keys: "Clés", format: "Format", noPlaceholders: "Aucun placeholder CHANGE_ME trouvé.", language: "Langue", theme: "Apparence", system: "Système", light: "Clair", dark: "Sombre", version: "Version", author: "Auteur", repository: "Dépôt" }, nl: { appSubtitle: "Lokale helper voor .env sjablonen", loadFile: "Bestand laden", saveFile: ".env opslaan", settings: "Instellingen", input: "Input", output: "Output", inputHint: "Laad een bestand of bewerk tekst direct", outputHint: "Gegenereerde .env, klaar om te kopiëren", copy: "Kopiëren", copied: "Gekopieerd", regenerate: "Opnieuw maken", hits: "Treffers", values: "Waarden", defaultsApplied: "Defaults", source: "Bron", file: "Bestand", text: "Tekst", defaults: "Standaardwaarden", defaultsHint: "Mogelijke standaardwaarden worden herkend; eigen waarden overschrijven keys.", addDefault: "Waarde toevoegen", autoDefault: "Auto", manualDefault: "Handmatig", key: "Key", value: "Waarde", detected: "Gedetecteerde eisen", detectedHint: "Placeholder, betrokken keys en gegenereerd formaat.", placeholder: "Placeholder", keys: "Keys", format: "Formaat", noPlaceholders: "Geen CHANGE_ME placeholders gevonden.", language: "Taal", theme: "Weergave", system: "Systeem", light: "Licht", dark: "Donker", version: "Versie", author: "Auteur", repository: "Repository" } } satisfies Record>; const themeLabels: ThemeMode[] = ["system", "light", "dark"]; function detectLanguage(): Language { const stored = localStorage.getItem("envhelper-language") as Language | null; if (stored && stored in languageNames) { return stored; } const browserLanguage = navigator.language.slice(0, 2) as Language; return browserLanguage in languageNames ? browserLanguage : "en"; } function readDefaults(): EnvDefault[] { const stored = localStorage.getItem("envhelper-defaults"); if (!stored) { return initialDefaults; } try { const parsed = JSON.parse(stored) as EnvDefault[]; return Array.isArray(parsed) ? parsed.filter((entry) => entry.key && typeof entry.value === "string" && canPersistDefault(entry.key)) : initialDefaults; } catch { return initialDefaults; } } function canPersistDefault(key: string): boolean { const signal = normalizeKey(key); return !["PASSWORD", "SECRET", "TOKEN", "PRIVATE_KEY", "API_KEY", "ACCESS_KEY"].some((marker) => signal.includes(marker)); } function readIgnoredAutoDefaults(): string[] { const stored = localStorage.getItem("envhelper-ignored-auto-defaults"); if (!stored) { return []; } try { const parsed = JSON.parse(stored) as string[]; return Array.isArray(parsed) ? parsed.filter(Boolean) : []; } catch { return []; } } function readTheme(): ThemeMode { const stored = localStorage.getItem("envhelper-theme") as ThemeMode | null; return stored && themeLabels.includes(stored) ? stored : "system"; } function inferAutomaticDefaults(input: string, manualDefaults: EnvDefault[], ignoredKeys: string[]): EnvDefault[] { const manualKeys = new Set(manualDefaults.map((entry) => normalizeKey(entry.key)).filter(Boolean)); const ignored = new Set(ignoredKeys.map(normalizeKey).filter(Boolean)); const defaults = new Map(); for (const line of input.split(/\r?\n/)) { const parsed = parseEnvAssignment(line); if (!parsed) { continue; } const normalizedKey = normalizeKey(parsed.key); if (!normalizedKey || manualKeys.has(normalizedKey) || ignored.has(normalizedKey)) { continue; } const value = inferDefaultValue(parsed.key, parsed.value); if (value) { defaults.set(normalizedKey, { key: parsed.key, value }); } } return [...defaults.values()].sort((a, b) => a.key.localeCompare(b.key)); } function parseEnvAssignment(line: string): { key: string; value: string } | null { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { return null; } const normalized = trimmed.startsWith("export ") ? trimmed.slice(7).trimStart() : trimmed; const equalsIndex = normalized.indexOf("="); if (equalsIndex <= 0) { return null; } return { key: normalized.slice(0, equalsIndex).trim(), value: normalized.slice(equalsIndex + 1).trim() }; } function inferDefaultValue(key: string, value: string): string | null { const signal = normalizeKey(key); const hasPlaceholder = /CHANGE_ME[A-Z0-9_]*/i.test(value); const isEmpty = value.length === 0; const shouldFillGeneric = hasPlaceholder || isEmpty; if (signal === "APP_ENV" || signal === "RAILS_ENV") { return "production"; } if (signal === "NODE_ENV") { return "production"; } if (signal.includes("LOG_LEVEL")) { return "info"; } if (signal.includes("TELEMETRY_ENABLED") || signal.includes("PROMETHEUS_METRICS") || signal.includes("METRICS_ENABLED")) { return "false"; } if (signal.includes("CORS_CREDENTIALS") || signal.includes("USE_SSL") || signal.includes("REQUIRE_SSL")) { return "true"; } if (signal.includes("BOOTSTRAP_ADMIN_EMAIL") || signal.includes("ADMIN_EMAIL")) { return "admin@example.local"; } if (signal.includes("SUPPORT_EMAIL")) { return "support@example.local"; } if (signal.includes("CONTACT_EMAIL")) { return "contact@example.local"; } if (shouldFillGeneric && signal.endsWith("EMAIL")) { return "admin@example.local"; } if (shouldFillGeneric && (signal.includes("BASE_URL") || signal.endsWith("_URL") || signal.endsWith("_URI") || signal.includes("ORIGIN") || signal.includes("SITE_URL"))) { return "https://example.local"; } if (shouldFillGeneric && signal.includes("CORS_ORIGIN")) { return "https://example.local"; } if (shouldFillGeneric && signal.includes("SMTP_HOST")) { return "smtp.example.local"; } if (shouldFillGeneric && signal.includes("SMTP_PORT")) { return "587"; } if (shouldFillGeneric && signal.includes("SMTP_USER")) { return "smtp-user"; } if (shouldFillGeneric && (signal.includes("SMTP_FROM") || signal.includes("MAIL_FROM") || signal.includes("SENDER_EMAIL"))) { return "noreply@example.local"; } if (shouldFillGeneric && (signal.includes("S3_BUCKET") || signal.includes("BUCKET_NAME"))) { return "app-bucket"; } if (shouldFillGeneric && (signal.includes("S3_REGION") || signal.includes("AWS_REGION") || signal.includes("SQS_REGION"))) { return "eu-central-1"; } if (shouldFillGeneric && signal.includes("S3_ENDPOINT")) { return "https://s3.example.local"; } if (shouldFillGeneric && signal.includes("RABBITMQ_URI")) { return "amqp://rabbitmq:5672"; } if (shouldFillGeneric && signal.includes("REDIS_URL")) { return "redis://redis:6379/0"; } if (shouldFillGeneric && signal.includes("PORT")) { if (signal.includes("POSTGRES")) { return "5432"; } if (signal.includes("REDIS")) { return "6379"; } if (signal.includes("SMTP")) { return "587"; } if (signal.includes("S3") || signal.includes("MINIO")) { return "9000"; } return "3000"; } return null; } function normalizeKey(key: string): string { return key.trim().toUpperCase(); } function addUnique(values: string[], value: string): string[] { const normalized = normalizeKey(value); return values.some((entry) => normalizeKey(entry) === normalized) ? values : [...values, value]; } function openFileWithBrowserPicker(): Promise<{ name: string; content: string } | null> { return new Promise((resolve) => { const input = document.createElement("input"); input.type = "file"; input.accept = ".env,.txt,text/plain"; input.addEventListener( "change", async () => { const file = input.files?.[0]; if (!file) { resolve(null); return; } resolve({ name: file.name, content: await file.text() }); }, { once: true } ); input.click(); }); } function saveFileWithBrowserDownload(content: string) { const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = ".env"; anchor.style.display = "none"; document.body.append(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); } export default function App() { const [input, setInput] = useState(""); const [loadedPath, setLoadedPath] = useState(null); const [copyLabel, setCopyLabel] = useState<"copy" | "copied">("copy"); const [generation, setGeneration] = useState(0); const [settingsOpen, setSettingsOpen] = useState(false); const [themeMode, setThemeMode] = useState(readTheme); const [language, setLanguage] = useState(detectLanguage); const [manualDefaults, setManualDefaults] = useState(readDefaults); const [ignoredAutoDefaults, setIgnoredAutoDefaults] = useState(readIgnoredAutoDefaults); const t = translations[language]; const automaticDefaults = useMemo(() => inferAutomaticDefaults(input, manualDefaults, ignoredAutoDefaults), [input, manualDefaults, ignoredAutoDefaults]); const defaults = useMemo( () => [ ...automaticDefaults.map((entry) => ({ ...entry, source: "auto" as const })), ...manualDefaults.map((entry, index) => ({ ...entry, index, source: "manual" as const })) ], [automaticDefaults, manualDefaults] ); const result = useMemo(() => transformEnv(input, defaults), [input, defaults, generation]); useEffect(() => { document.documentElement.dataset.theme = themeMode; localStorage.setItem("envhelper-theme", themeMode); }, [themeMode]); useEffect(() => { localStorage.setItem("envhelper-language", language); }, [language]); useEffect(() => { localStorage.setItem("envhelper-defaults", JSON.stringify(manualDefaults.filter((entry) => canPersistDefault(entry.key)))); }, [manualDefaults]); useEffect(() => { localStorage.setItem("envhelper-ignored-auto-defaults", JSON.stringify(ignoredAutoDefaults)); }, [ignoredAutoDefaults]); async function openFile() { try { if (window.envHelper?.openFile) { const file = await window.envHelper.openFile(); if (file) { setInput(file.content); setLoadedPath(file.path); } return; } } catch (error) { console.warn("Native file dialog failed, falling back to browser file input.", error); } const file = await openFileWithBrowserPicker(); if (file) { setInput(file.content); setLoadedPath(file.name); } } async function saveFile() { try { if (window.envHelper?.saveFile) { await window.envHelper.saveFile(result.output); return; } } catch (error) { console.warn("Native save dialog failed, falling back to browser download.", error); } saveFileWithBrowserDownload(result.output); } async function copyOutput() { await navigator.clipboard.writeText(result.output); setCopyLabel("copied"); window.setTimeout(() => setCopyLabel("copy"), 1400); } function updateDefault(row: DefaultRow, field: keyof EnvDefault, value: string) { if (row.source === "auto") { setIgnoredAutoDefaults((current) => addUnique(current, row.key)); setManualDefaults((current) => [...current, { key: field === "key" ? value : row.key, value: field === "value" ? value : row.value }]); return; } if (row.index === undefined) { return; } setManualDefaults((current) => current.map((entry, entryIndex) => (entryIndex === row.index ? { ...entry, [field]: value } : entry))); } function addDefault() { setManualDefaults((current) => [...current, { key: "", value: "" }]); } function removeDefault(row: DefaultRow) { if (row.source === "auto") { setIgnoredAutoDefaults((current) => addUnique(current, row.key)); return; } if (row.index === undefined) { return; } setManualDefaults((current) => current.filter((_, entryIndex) => entryIndex !== row.index)); } return (
EH

EnvHelper

{t.appSubtitle}

{themeLabels.map((mode) => ( ))}