/**
* 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 };
}