103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
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<string, string | undefined> | 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 <key>${plistEscape(key)}</key>\n <string>${plistEscape(value?.trim() ?? "")}</string>`,
|
|
)
|
|
.join("");
|
|
return `\n <key>EnvironmentVariables</key>\n <dict>${items}\n </dict>`;
|
|
};
|
|
|
|
export async function readLaunchAgentProgramArgumentsFromFile(plistPath: string): Promise<{
|
|
programArguments: string[];
|
|
workingDirectory?: string;
|
|
environment?: Record<string, string>;
|
|
sourcePath?: string;
|
|
} | null> {
|
|
try {
|
|
const plist = await fs.readFile(plistPath, "utf8");
|
|
const programMatch = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/i);
|
|
if (!programMatch) return null;
|
|
const args = Array.from(programMatch[1].matchAll(/<string>([\s\S]*?)<\/string>/gi)).map(
|
|
(match) => plistUnescape(match[1] ?? "").trim(),
|
|
);
|
|
const workingDirMatch = plist.match(
|
|
/<key>WorkingDirectory<\/key>\s*<string>([\s\S]*?)<\/string>/i,
|
|
);
|
|
const workingDirectory = workingDirMatch ? plistUnescape(workingDirMatch[1] ?? "").trim() : "";
|
|
const envMatch = plist.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/i);
|
|
const environment: Record<string, string> = {};
|
|
if (envMatch) {
|
|
for (const pair of envMatch[1].matchAll(
|
|
/<key>([\s\S]*?)<\/key>\s*<string>([\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, string | undefined>;
|
|
}): string {
|
|
const argsXml = programArguments
|
|
.map((arg) => `\n <string>${plistEscape(arg)}</string>`)
|
|
.join("");
|
|
const workingDirXml = workingDirectory
|
|
? `\n <key>WorkingDirectory</key>\n <string>${plistEscape(workingDirectory)}</string>`
|
|
: "";
|
|
const commentXml = comment?.trim()
|
|
? `\n <key>Comment</key>\n <string>${plistEscape(comment.trim())}</string>`
|
|
: "";
|
|
const envXml = renderEnvDict(environment);
|
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n <dict>\n <key>Label</key>\n <string>${plistEscape(label)}</string>\n ${commentXml}\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <true/>\n <key>ProgramArguments</key>\n <array>${argsXml}\n </array>\n ${workingDirXml}\n <key>StandardOutPath</key>\n <string>${plistEscape(stdoutPath)}</string>\n <key>StandardErrorPath</key>\n <string>${plistEscape(stderrPath)}</string>${envXml}\n </dict>\n</plist>\n`;
|
|
}
|