From 0d4c6e9c827ffdb77aab683a57959447c2239474 Mon Sep 17 00:00:00 2001 From: MrSphay Date: Fri, 1 May 2026 12:54:29 +0200 Subject: [PATCH] Build EnvHelper desktop app --- .gitea/workflows/build-windows.yml | 51 +++++ .gitignore | 8 + README.md | 34 ++++ electron/main.ts | 74 +++++++ electron/preload.ts | 6 + index.html | 12 ++ package.json | 55 ++++++ src/App.tsx | 150 +++++++++++++++ src/env.ts | 220 +++++++++++++++++++++ src/main.tsx | 10 + src/styles.css | 300 +++++++++++++++++++++++++++++ src/vite-env.d.ts | 13 ++ tsconfig.electron.json | 15 ++ tsconfig.json | 20 ++ vite.config.ts | 10 + 15 files changed, 978 insertions(+) create mode 100644 .gitea/workflows/build-windows.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 electron/main.ts create mode 100644 electron/preload.ts create mode 100644 index.html create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/env.ts create mode 100644 src/main.tsx create mode 100644 src/styles.css create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.electron.json create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitea/workflows/build-windows.yml b/.gitea/workflows/build-windows.yml new file mode 100644 index 0000000..7b7e0ea --- /dev/null +++ b/.gitea/workflows/build-windows.yml @@ -0,0 +1,51 @@ +name: Build Windows App + +on: + push: + branches: + - main + - master + workflow_dispatch: + +jobs: + build-windows: + runs-on: windows-latest + env: + GH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + GITHUB_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: npm install + + - name: Build Windows installer + run: npm run dist:win + + - name: Upload Windows artifacts + uses: actions/upload-artifact@v4 + with: + name: envhelper-windows + path: release/* + + - name: Publish to Gitea package registry + shell: pwsh + run: | + $package = Get-Content package.json | ConvertFrom-Json + $shortSha = $env:GITHUB_SHA.Substring(0, 7) + $packageVersion = "$($package.version)-$shortSha" + $headers = @{ Authorization = "token $env:REGISTRY_TOKEN" } + + Get-ChildItem release -File | ForEach-Object { + $fileName = [System.Uri]::EscapeDataString($_.Name) + $uri = "https://git.wilkensxl.de/api/packages/MrSphay/generic/envhelper/$packageVersion/$fileName" + Invoke-RestMethod -Method Put -Uri $uri -Headers $headers -InFile $_.FullName -ContentType "application/octet-stream" + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23928fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +dist +dist-electron +release +*.log +.env +.env.* +!.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4c54ce --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# EnvHelper + +EnvHelper ist eine Windows-Desktop-App zum Ersetzen von `CHANGE_ME` Platzhaltern in `.env` Dateien. + +## Funktionen + +- `.env` Datei laden oder reinen Text einfügen +- `CHANGE_ME...` Platzhalter erkennen +- passende Werte anhand von Variablenname und Platzhalter ableiten +- gleiche Platzhalter konsistent mit demselben Wert ersetzen +- neue `.env` als Text kopieren oder speichern + +## Entwicklung + +```bash +npm install +npm run dev +``` + +In einem zweiten Terminal: + +```bash +npm run build +``` + +## Windows Build + +Der Windows-Build läuft über Gitea Actions: + +```bash +npm run dist:win +``` + +Die erzeugten Dateien liegen im Runner unter `release/` und werden als Workflow-Artefakt hochgeladen. diff --git a/electron/main.ts b/electron/main.ts new file mode 100644 index 0000000..12d3409 --- /dev/null +++ b/electron/main.ts @@ -0,0 +1,74 @@ +import { app, BrowserWindow, dialog, ipcMain } from "electron"; +import isDev from "electron-is-dev"; +import { readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +async function createWindow() { + const win = new BrowserWindow({ + width: 1220, + height: 820, + minWidth: 980, + minHeight: 680, + title: "EnvHelper", + backgroundColor: "#f6f7f4", + webPreferences: { + preload: path.join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false + } + }); + + if (isDev) { + await win.loadURL("http://127.0.0.1:5173"); + } else { + await win.loadFile(path.join(__dirname, "../dist/index.html")); + } +} + +ipcMain.handle("envhelper:open-file", async () => { + const result = await dialog.showOpenDialog({ + properties: ["openFile"], + filters: [{ name: "Environment files", extensions: ["env", "txt", "*"] }] + }); + + if (result.canceled || result.filePaths.length === 0) { + return null; + } + + const filePath = result.filePaths[0]; + return { + path: filePath, + content: await readFile(filePath, "utf8") + }; +}); + +ipcMain.handle("envhelper:save-file", async (_event, content: string) => { + const result = await dialog.showSaveDialog({ + defaultPath: ".env", + filters: [{ name: "Environment file", extensions: ["env"] }] + }); + + if (result.canceled || !result.filePath) { + return null; + } + + await writeFile(result.filePath, content, "utf8"); + return result.filePath; +}); + +app.whenReady().then(createWindow); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + void createWindow(); + } +}); diff --git a/electron/preload.ts b/electron/preload.ts new file mode 100644 index 0000000..cf5fa72 --- /dev/null +++ b/electron/preload.ts @@ -0,0 +1,6 @@ +import { contextBridge, ipcRenderer } from "electron"; + +contextBridge.exposeInMainWorld("envHelper", { + openFile: () => ipcRenderer.invoke("envhelper:open-file"), + saveFile: (content: string) => ipcRenderer.invoke("envhelper:save-file", content) +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..5ba5ac3 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + EnvHelper + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..3a5dd02 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "envhelper", + "version": "0.1.0", + "description": "Desktop helper for replacing CHANGE_ME placeholders in .env files.", + "author": "MrSphay", + "private": true, + "main": "dist-electron/main.js", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "tsc --noEmit && vite build && tsc -p tsconfig.electron.json", + "dist:win": "npm run build && electron-builder --win nsis portable --x64", + "lint": "tsc --noEmit" + }, + "dependencies": { + "electron-is-dev": "^3.0.1", + "lucide-react": "^0.468.0", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "electron": "^38.1.2", + "electron-builder": "^26.0.12", + "typescript": "^5.9.2", + "vite": "^7.1.7" + }, + "build": { + "appId": "de.wilkensxl.envhelper", + "productName": "EnvHelper", + "directories": { + "output": "release" + }, + "files": [ + "dist/**/*", + "dist-electron/**/*", + "package.json" + ], + "win": { + "target": [ + "nsis", + "portable" + ], + "artifactName": "EnvHelper-${version}-${arch}.${ext}" + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true + } + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..eb74a73 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,150 @@ +import { Check, Clipboard, FileDown, FileInput, RefreshCcw, ShieldCheck, Sparkles } from "lucide-react"; +import { useMemo, useState } from "react"; +import { transformEnv } from "./env"; + +const sampleEnv = `APP_IMAGE=git.wilkensxl.de/mrsphay/hilden-directory-gateway:latest +APP_PORT=3000 + +NODE_ENV=production +PUBLIC_BASE_URL=https://gateway.example.local +TRUSTED_IFRAME_ANCESTORS=https://www.hilden.de + +DATABASE_URL=postgresql://hilden_app:CHANGE_ME_POSTGRES_PASSWORD@postgres:5432/hilden_directory +POSTGRES_PASSWORD=CHANGE_ME_POSTGRES_PASSWORD + +SESSION_SECRET=CHANGE_ME_AT_LEAST_32_RANDOM_CHARACTERS +ENCRYPTION_KEY_BASE64=CHANGE_ME_32_RANDOM_BYTES_AS_BASE64 + +BOOTSTRAP_ADMIN_EMAIL=admin@example.local +BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_LONG_INITIAL_ADMIN_PASSWORD`; + +export default function App() { + const [input, setInput] = useState(sampleEnv); + const [loadedPath, setLoadedPath] = useState(null); + const [copyLabel, setCopyLabel] = useState("Kopieren"); + const [generation, setGeneration] = useState(0); + const result = useMemo(() => transformEnv(input), [input, generation]); + + async function openFile() { + const file = await window.envHelper?.openFile(); + if (file) { + setInput(file.content); + setLoadedPath(file.path); + } + } + + async function saveFile() { + await window.envHelper?.saveFile(result.output); + } + + async function copyOutput() { + await navigator.clipboard.writeText(result.output); + setCopyLabel("Kopiert"); + window.setTimeout(() => setCopyLabel("Kopieren"), 1400); + } + + function regenerate() { + setGeneration((current) => current + 1); + } + + return ( +
+
+
+

EnvHelper

+

.env Platzhalter sauber ersetzen

+

+ Erkennt CHANGE_ME Werte, erzeugt passende Secrets im richtigen Format und hält gleiche Platzhalter synchron. +

+
+
+ + +
+
+ +
+
+ {result.changedCount} +

Fundstellen

+
+
+ {result.replacements.length} +

eindeutige Werte

+
+
+ {loadedPath ? "Datei" : "Text"} +

Quelle

+
+
+ +
+
+
+
+

Input

+

{loadedPath ?? "Direkt einfügen oder Beispieldaten ersetzen"}

+
+ +
+