/** * macOS service controller — installs a launchd LaunchAgent that runs at login * and is kept alive automatically. */ import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { runSafe } from "./platform.js"; import type { LaunchSpec, ServiceController, ServiceResult } from "./types.js"; const LABEL = "com.grok.telegrambot"; function labelOf(spec?: { macosLabel?: string }): string { return spec?.macosLabel || LABEL; } function plistPath(spec?: { macosLabel?: string }): string { return join(homedir(), "Library", "LaunchAgents", `${labelOf(spec)}.plist`); } export const macosController: ServiceController = { platform: "macos", async install(spec) { mkdirSync(join(homedir(), "Library", "LaunchAgents"), { recursive: true }); mkdirSync(spec.logsDir, { recursive: true }); const path = plistPath(spec); runSafe("launchctl", ["unload", "-w", path]); // ignore if not loaded writeFileSync(path, plist(spec), "utf-8"); const r = runSafe("launchctl", ["load", "-w", path]); return r.ok ? ok(`Installed and loaded LaunchAgent "${labelOf(spec)}".`) : fail(r.out); }, async uninstall(spec) { runSafe("launchctl", ["unload", "-w", plistPath(spec)]); rmSync(plistPath(spec), { force: true }); return ok(`Removed LaunchAgent "${labelOf(spec)}".`); }, async start(spec) { const r = runSafe("launchctl", ["start", labelOf(spec)]); return r.ok ? ok("Started.") : fail(r.out); }, async stop(spec) { const r = runSafe("launchctl", ["stop", labelOf(spec)]); return r.ok ? ok("Stopped.") : fail(r.out); }, async status(spec) { const lab = labelOf(spec); const r = runSafe("launchctl", ["list"]); const line = r.out.split("\n").find((l) => l.includes(lab)); return ok(line ? `Loaded: ${line.trim()}` : "Not loaded."); }, }; function plist(spec: LaunchSpec): string { const args = [spec.nodePath, ...spec.args].map((a) => ` ${esc(a)}`).join("\n"); const envEntries = Object.entries(spec.env ?? {}); const envBlock = envEntries.length ? [ " EnvironmentVariables", " ", ...envEntries.flatMap(([k, v]) => [` ${esc(k)}`, ` ${esc(v)}`]), " ", ] : []; return [ '', '', '', "", " Label", ` ${labelOf(spec)}`, " ProgramArguments", " ", args, " ", ...envBlock, " WorkingDirectory", ` ${esc(spec.cwd)}`, " RunAtLoad", " ", " KeepAlive", " ", " StandardOutPath", ` ${esc(spec.logFile)}`, " StandardErrorPath", ` ${esc(spec.logFile)}`, "", "", "", ].join("\n"); } function esc(s: string): string { return s.replace(/&/g, "&").replace(//g, ">"); } function ok(message: string): ServiceResult { return { ok: true, message }; } function fail(message: string): ServiceResult { return { ok: false, message }; }