Allow configuring environmental variables in panel (#626)

- [ ] Add test cases
This commit is contained in:
Will Chen
2025-07-11 10:52:52 -07:00
committed by GitHub
parent 4b84b12fe3
commit 2c284d0f20
18 changed files with 1212 additions and 7 deletions

View File

@@ -0,0 +1,81 @@
/**
* DO NOT USE LOGGER HERE.
* Environment variables are sensitive and should not be logged.
*/
import { ipcMain } from "electron";
import * as fs from "fs";
import * as path from "path";
import { db } from "../../db";
import { apps } from "../../db/schema";
import { eq } from "drizzle-orm";
import { getDyadAppPath } from "../../paths/paths";
import { GetAppEnvVarsParams, SetAppEnvVarsParams } from "../ipc_types";
import { parseEnvFile, serializeEnvFile } from "../utils/app_env_var_utils";
export function registerAppEnvVarsHandlers() {
// Handler to get app environment variables
ipcMain.handle(
"get-app-env-vars",
async (event, { appId }: GetAppEnvVarsParams) => {
try {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
const envFilePath = path.join(appPath, ".env.local");
// If .env.local doesn't exist, return empty array
try {
await fs.promises.access(envFilePath);
} catch {
return [];
}
const content = await fs.promises.readFile(envFilePath, "utf8");
const envVars = parseEnvFile(content);
return envVars;
} catch (error) {
console.error("Error getting app environment variables:", error);
throw new Error(
`Failed to get environment variables: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
},
);
// Handler to set app environment variables
ipcMain.handle(
"set-app-env-vars",
async (event, { appId, envVars }: SetAppEnvVarsParams) => {
try {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
const envFilePath = path.join(appPath, ".env.local");
// Serialize environment variables to .env.local format
const content = serializeEnvFile(envVars);
// Write to .env.local file
await fs.promises.writeFile(envFilePath, content, "utf8");
} catch (error) {
console.error("Error setting app environment variables:", error);
throw new Error(
`Failed to set environment variables: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
},
);
}

View File

@@ -38,6 +38,8 @@ import type {
AppUpgrade,
ProblemReport,
EditAppFileReturnType,
GetAppEnvVarsParams,
SetAppEnvVarsParams,
} from "./ipc_types";
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
import { showError } from "@/lib/toast";
@@ -183,6 +185,16 @@ export class IpcClient {
return this.ipcRenderer.invoke("get-app", appId);
}
public async getAppEnvVars(
params: GetAppEnvVarsParams,
): Promise<{ key: string; value: string }[]> {
return this.ipcRenderer.invoke("get-app-env-vars", params);
}
public async setAppEnvVars(params: SetAppEnvVarsParams): Promise<void> {
return this.ipcRenderer.invoke("set-app-env-vars", params);
}
public async getChat(chatId: number): Promise<Chat> {
try {
const data = await this.ipcRenderer.invoke("get-chat", chatId);

View File

@@ -23,6 +23,7 @@ import { registerContextPathsHandlers } from "./handlers/context_paths_handlers"
import { registerAppUpgradeHandlers } from "./handlers/app_upgrade_handlers";
import { registerCapacitorHandlers } from "./handlers/capacitor_handlers";
import { registerProblemsHandlers } from "./handlers/problems_handlers";
import { registerAppEnvVarsHandlers } from "./handlers/app_env_vars_handlers";
export function registerIpcHandlers() {
// Register all IPC handlers by category
@@ -51,4 +52,5 @@ export function registerIpcHandlers() {
registerContextPathsHandlers();
registerAppUpgradeHandlers();
registerCapacitorHandlers();
registerAppEnvVarsHandlers();
}

View File

@@ -252,3 +252,17 @@ export interface AppUpgrade {
export interface EditAppFileReturnType {
warning?: string;
}
export interface EnvVar {
key: string;
value: string;
}
export interface SetAppEnvVarsParams {
appId: number;
envVars: EnvVar[];
}
export interface GetAppEnvVarsParams {
appId: number;
}

View File

@@ -0,0 +1,71 @@
/**
* DO NOT USE LOGGER HERE.
* Environment variables are sensitive and should not be logged.
*/
import { EnvVar } from "../ipc_types";
// Helper function to parse .env.local file content
export function parseEnvFile(content: string): EnvVar[] {
const envVars: EnvVar[] = [];
const lines = content.split("\n");
for (const line of lines) {
const trimmedLine = line.trim();
// Skip empty lines and comments
if (!trimmedLine || trimmedLine.startsWith("#")) {
continue;
}
// Parse key=value pairs
const equalIndex = trimmedLine.indexOf("=");
if (equalIndex > 0) {
const key = trimmedLine.substring(0, equalIndex).trim();
const value = trimmedLine.substring(equalIndex + 1).trim();
// Handle quoted values with potential inline comments
let cleanValue = value;
if (value.startsWith('"')) {
// Find the closing quote, handling escaped quotes
let endQuoteIndex = -1;
for (let i = 1; i < value.length; i++) {
if (value[i] === '"' && value[i - 1] !== "\\") {
endQuoteIndex = i;
break;
}
}
if (endQuoteIndex !== -1) {
cleanValue = value.slice(1, endQuoteIndex);
// Unescape escaped quotes
cleanValue = cleanValue.replace(/\\"/g, '"');
}
} else if (value.startsWith("'")) {
// Find the closing quote for single quotes
const endQuoteIndex = value.indexOf("'", 1);
if (endQuoteIndex !== -1) {
cleanValue = value.slice(1, endQuoteIndex);
}
}
// For unquoted values, keep everything as-is (including potential # symbols)
envVars.push({ key, value: cleanValue });
}
}
return envVars;
}
// Helper function to serialize environment variables to .env.local format
export function serializeEnvFile(envVars: EnvVar[]): string {
return envVars
.map(({ key, value }) => {
// Add quotes if value contains spaces or special characters
const needsQuotes = /[\s#"'=&?]/.test(value);
const quotedValue = needsQuotes
? `"${value.replace(/"/g, '\\"')}"`
: value;
return `${key}=${quotedValue}`;
})
.join("\n");
}