import fs from "node:fs/promises"; const plistEscape = (value: string): string => value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const plistUnescape = (value: string): string => value .replaceAll("'", "'") .replaceAll(""", '"') .replaceAll(">", ">") .replaceAll("<", "<") .replaceAll("&", "&"); const renderEnvDict = (env: Record | undefined): string => { if (!env) return ""; const entries = Object.entries(env).filter( ([, value]) => typeof value === "string" && value.trim(), ); if (entries.length === 0) return ""; const items = entries .map( ([key, value]) => `\n ${plistEscape(key)}\n ${plistEscape(value?.trim() ?? "")}`, ) .join(""); return `\n EnvironmentVariables\n ${items}\n `; }; export async function readLaunchAgentProgramArgumentsFromFile(plistPath: string): Promise<{ programArguments: string[]; workingDirectory?: string; environment?: Record; sourcePath?: string; } | null> { try { const plist = await fs.readFile(plistPath, "utf8"); const programMatch = plist.match(/ProgramArguments<\/key>\s*([\s\S]*?)<\/array>/i); if (!programMatch) return null; const args = Array.from(programMatch[1].matchAll(/([\s\S]*?)<\/string>/gi)).map( (match) => plistUnescape(match[1] ?? "").trim(), ); const workingDirMatch = plist.match( /WorkingDirectory<\/key>\s*([\s\S]*?)<\/string>/i, ); const workingDirectory = workingDirMatch ? plistUnescape(workingDirMatch[1] ?? "").trim() : ""; const envMatch = plist.match(/EnvironmentVariables<\/key>\s*([\s\S]*?)<\/dict>/i); const environment: Record = {}; if (envMatch) { for (const pair of envMatch[1].matchAll( /([\s\S]*?)<\/key>\s*([\s\S]*?)<\/string>/gi, )) { const key = plistUnescape(pair[1] ?? "").trim(); if (!key) continue; const value = plistUnescape(pair[2] ?? "").trim(); environment[key] = value; } } return { programArguments: args.filter(Boolean), ...(workingDirectory ? { workingDirectory } : {}), ...(Object.keys(environment).length > 0 ? { environment } : {}), sourcePath: plistPath, }; } catch { return null; } } export function buildLaunchAgentPlist({ label, comment, programArguments, workingDirectory, stdoutPath, stderrPath, environment, }: { label: string; comment?: string; programArguments: string[]; workingDirectory?: string; stdoutPath: string; stderrPath: string; environment?: Record; }): string { const argsXml = programArguments .map((arg) => `\n ${plistEscape(arg)}`) .join(""); const workingDirXml = workingDirectory ? `\n WorkingDirectory\n ${plistEscape(workingDirectory)}` : ""; const commentXml = comment?.trim() ? `\n Comment\n ${plistEscape(comment.trim())}` : ""; const envXml = renderEnvDict(environment); return `\n\n\n \n Label\n ${plistEscape(label)}\n ${commentXml}\n RunAtLoad\n \n KeepAlive\n \n ProgramArguments\n ${argsXml}\n \n ${workingDirXml}\n StandardOutPath\n ${plistEscape(stdoutPath)}\n StandardErrorPath\n ${plistEscape(stderrPath)}${envXml}\n \n\n`; }