Files
clawdbot/src/daemon/launchd-plist.ts
Peter Steinberger c379191f80 chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
2026-01-14 15:02:19 +00:00

103 lines
3.9 KiB
TypeScript

import fs from "node:fs/promises";
const plistEscape = (value: string): string =>
value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&apos;");
const plistUnescape = (value: string): string =>
value
.replaceAll("&apos;", "'")
.replaceAll("&quot;", '"')
.replaceAll("&gt;", ">")
.replaceAll("&lt;", "<")
.replaceAll("&amp;", "&");
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`;
}