import { existsSync } from 'node:fs'; import { PLATFORM_DIR } from './data-path'; import { readEcosystemApps, addCoreProcess, type Pm2App } from './plugins/ecosystem'; import { detectPackageManager, installCommand, manualInstallHint, sudoAvailable } from './plugins/os-deps'; // Bringing an EXISTING server up to the 2026-08-19 layout, where calendar and contacts are core. // // A fresh install needs none of this: `officer-setup` writes officer-caldav into the ecosystem from // CORE_PROCESSES, and machine-setup installs `radicale` with the other packages. But a server set up // before today has neither, and `git pull` changes no state outside the repository — so without this a // pull would leave `/calendar` mounted, working, and talking to nothing, which is precisely the silent // half-working failure this codebase keeps being bitten by. // // ── Why it does not block the boot ── // // Everything here is best-effort and runs AFTER `serve()`. A package manager holding a lock, an // unreachable mirror or a machine with no passwordless sudo must not stop Officer from starting. The // worst outcome is that calendar does not work and the log says why, which is the same outcome as doing // nothing — only louder. const RADICALE_ENTRY: Pm2App = { name: 'officer-caldav', script: 'bun', args: 'run src/servers/sidecar/caldav/index.ts', cwd: PLATFORM_DIR, watch: false, }; /** Is Radicale on PATH? The sidecar spawns it by bare name, so this is the same question it will ask. */ export function radicaleInstalled(): boolean { return Bun.which('radicale') !== null; } /** * Add `officer-caldav` to the ecosystem file if a pre-2026-08-19 install lacks it. * * Returns true when it wrote. PM2 still has to be told — `pm2 start ecosystem.config.cjs * --only officer-caldav` — because this process cannot restart its own supervisor's view of the file. */ export function ensureCaldavProcess(): boolean { const apps = readEcosystemApps(); if (apps.some((a) => a.name === RADICALE_ENTRY.name)) return false; addCoreProcess(RADICALE_ENTRY); return true; } /** * Install Radicale when it is missing and the platform is allowed to. * * `sudo -n` only, never a prompt: this runs unattended under PM2, where a password prompt is not a slow * path but a hang. When it cannot, it prints the exact command for a person to run rather than failing * silently — the whole point is that the operator finds out from the log rather than from a phone that * stopped syncing. */ export async function ensureRadicale(): Promise<'present' | 'installed' | 'blocked' | 'failed'> { if (radicaleInstalled()) return 'present'; const manager = detectPackageManager(); if (!manager || (manager !== 'brew' && !sudoAvailable())) { const hint = manualInstallHint(manager, ['radicale']); console.warn( `[caldav] radicale is not installed and the platform cannot install it` + `${manager ? '' : ' (no supported package manager found)'}.` + `${hint ? ` Run: ${hint}` : ''}`, ); return 'blocked'; } console.log('[caldav] radicale missing — installing'); const argv = installCommand(manager, ['radicale']); try { const proc = Bun.spawn(argv, { stdout: 'pipe', stderr: 'pipe' }); const code = await proc.exited; if (code !== 0 || !radicaleInstalled()) { const err = await new Response(proc.stderr).text(); console.error(`[caldav] radicale install failed (exit ${code}): ${err.trim().split('\n').slice(-3).join(' ')}`); return 'failed'; } } catch (err) { console.error(`[caldav] radicale install failed: ${err instanceof Error ? err.message : err}`); return 'failed'; } console.log('[caldav] radicale installed'); return 'installed'; } /** Both halves, in the order that makes the log read sensibly. Never throws. */ export async function bootstrapCaldav(): Promise { try { if (ensureCaldavProcess()) { console.log('[caldav] added officer-caldav to the ecosystem — run `pm2 start ecosystem.config.cjs`'); } await ensureRadicale(); } catch (err) { console.error('[caldav] bootstrap skipped —', err instanceof Error ? err.message : err); } } /** Exported for the test, which must not depend on this machine having an ecosystem file. */ export const _ecosystemPathExists = (): boolean => existsSync(`${PLATFORM_DIR}/ecosystem.config.cjs`);