import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { MAX_SOCKET_PATH_BYTES, RUNTIME_DIR_NAME, SOCKET_NAME, } from "./constants.ts"; export type RuntimePaths = { /** Canonical Darwin per-user temp + `pi-caps-blink-v1`. */ runtimeDir: string; /** Socket path: `{runtimeDir}/s`. */ socketPath: string; }; export class RuntimePathError extends Error { constructor(message: string) { super(message); this.name = "RuntimePathError"; } } export type ResolveRuntimePathsOptions = { /** Override `getconf DARWIN_USER_TEMP_DIR` result (tests). */ tempRoot?: string; /** Override realpath (tests). */ realpath?: (input: string) => string; }; /** * Canonical runtime directory: realpath(`/usr/bin/getconf DARWIN_USER_TEMP_DIR`) * + fixed `pi-caps-blink-v1`, with socket path UTF-8 length < 104. */ export function resolveRuntimePaths( options: ResolveRuntimePathsOptions = {}, ): RuntimePaths { const rawTemp = options.tempRoot ?? execFileSync("/usr/bin/getconf", ["DARWIN_USER_TEMP_DIR"], { encoding: "utf8", }).trim(); if (!rawTemp) { throw new RuntimePathError("getconf DARWIN_USER_TEMP_DIR returned empty"); } const realpath = options.realpath ?? ((p: string) => fs.realpathSync(p)); const tempRoot = realpath(rawTemp); const runtimeDir = path.join(tempRoot, RUNTIME_DIR_NAME); const socketPath = path.join(runtimeDir, SOCKET_NAME); const bytes = Buffer.byteLength(socketPath, "utf8"); if (bytes >= MAX_SOCKET_PATH_BYTES) { throw new RuntimePathError( `socket path is ${bytes} UTF-8 bytes; must be < ${MAX_SOCKET_PATH_BYTES}: ${socketPath}`, ); } return { runtimeDir, socketPath }; }