/** * `celilo subscribers install-daemon` — write a systemd unit (Linux) or * launchd plist (macOS) that runs the build-bus webhook receiver * (`celilo subscribers serve`) under supervision. Mirrors * `celilo events install-daemon` for the dispatcher: default is a * per-user unit; `--system` writes the system-scope unit that survives * logout/reboot — the management-plane shape. Writes the file but * doesn't touch supervisor state — the operator (or the celilo-mgmt * Ansible role) runs the enable/bootstrap steps themselves. * * The rendered unit runs `subscribers serve --no-dispatch` by default: * the standing dispatcher (celilo-events.service) owns dispatch on a * management box, and a second dispatcher polling the same bus is the * celilo#580/#610 failure. Pass --dispatch for a combined unit on a box * that has no events daemon. * * celilo#1304: nothing installed the receiver anywhere, so celilo-mgr's * on_upstream_publish self-update could never fire. This command is the * missing installer. */ import { installReceiverDaemon, planReceiverInstall, uninstallReceiverDaemon, } from '../../services/build-bus/receiver-daemon'; import type { CommandResult } from '../types'; import { resolveServeSecret } from './subscribers-serve'; function shortenPath(p: string): string { return p.startsWith(process.env.HOME ?? '\0') ? p.replace(process.env.HOME ?? '', '~') : p; } function daemonScopeFrom(flags: Record): 'user' | 'system' { return flags.system === true ? 'system' : 'user'; } export async function handleSubscribersInstallDaemon( _args: string[], flags: Record, env: Record = process.env, ): Promise { try { const options = { celiloPath: typeof flags['celilo-path'] === 'string' ? flags['celilo-path'] : undefined, port: flags.port ? Number(flags.port) : undefined, // Same resolution as `serve`: the flag wins, CELILO_BUS_SECRET is // the documented fallback (the doctor remediation names it), and // null means the renderer refuses rather than invent a secret. secret: resolveServeSecret(flags, env) ?? undefined, scope: daemonScopeFrom(flags), dispatch: flags.dispatch === true, }; // --print: render-only, raw unit content on stdout. The celilo-mgmt // Ansible role captures this and does the root-owned write itself // (the deb wrapper runs celilo as the unprivileged celilo user, so // a direct --system write would EACCES on /etc/systemd/system). if (flags.print) { const plan = planReceiverInstall(options); return { success: true, message: plan.unitContent, rawOutput: true, data: plan }; } const result = installReceiverDaemon(options); const lines = [ `Wrote ${result.platform} ${result.scope} unit: ${shortenPath(result.unitPath)}`, ` celilo: ${shortenPath(result.celiloPath)}`, ` port: ${result.port}`, ` dispatch: ${result.dispatch ? 'combined (receiver + hooks)' : 'receiver-only (--no-dispatch)'}`, ...(result.runAsUser ? [` runs as: ${result.runAsUser}`] : []), '', 'The unit embeds the shared secret the publisher must sign webhooks with —', "register this host in the publisher's build-bus-subscribers.json with the", 'same value (`celilo subscribers add --secret `).', '', 'Next steps (run these yourself — install does not touch supervisor state):', ...result.nextSteps.map((s) => ` ${s}`), ]; return { success: true, message: lines.join('\n'), data: result }; } catch (err) { return { success: false, error: err instanceof Error ? err.message : String(err), }; } } export async function handleSubscribersUninstallDaemon( _args: string[], flags: Record, ): Promise { try { const result = uninstallReceiverDaemon({ scope: daemonScopeFrom(flags) }); const lines = [ result.removed ? `Removed unit: ${shortenPath(result.unitPath)}` : `No unit file at ${shortenPath(result.unitPath)} — nothing to remove.`, '', 'Next steps:', ...result.nextSteps.map((s) => ` ${s}`), ]; return { success: true, message: lines.join('\n'), data: result }; } catch (err) { return { success: false, error: err instanceof Error ? err.message : String(err), }; } }