import { cp, readdir, rm } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { NOTIFICATIONS_DIR } from './catalogue'; import type { DiscoveredPlugin } from '../plugins/manifest'; // Copying a plugin's notification catalogue into the store the sidecar reads. // // Deliberately the same shape as `publishAffordances` — a directory in the plugin repository, copied to a // platform-managed place on install, removed on uninstall. Two mechanisms for "the plugin ships some // files" would drift, and that one is already understood. // // The destination namespace is what makes codes safe: `//`, mirroring `mountPrefix`, // so a plugin's codes are stamped with its provenance by WHERE IT LANDS rather than by anything it // declares. See `catalogue.ts`. /** Where a plugin's catalogue lands. `officerdev/music/`, beside `officer/core`'s built-in entries. */ export const notificationDirFor = (plugin: DiscoveredPlugin): string => join(NOTIFICATIONS_DIR, plugin.manifest.publisher, plugin.appName); /** What the plugin ships, if anything. */ export const notificationSourceFor = (plugin: DiscoveredPlugin): string => join(plugin.dir, 'notifications'); /** * Publish a plugin's catalogue. Returns the number of files, or 0 when it ships none — the ordinary case. * * Idempotent by overwriting, so re-running an install republishes rather than erroring. It does NOT remove * files the plugin has since dropped; that is uninstall's job, and deleting on every install would make a * half-finished copy destructive. */ export async function publishNotifications(plugin: DiscoveredPlugin): Promise { const source = notificationSourceFor(plugin); if (!existsSync(source)) return 0; const dest = notificationDirFor(plugin); await cp(source, dest, { recursive: true, force: true }); try { return ((await readdir(dest, { recursive: true })) as string[]).filter((f) => /\.ya?ml$/.test(f)).length; } catch { return 0; } } /** * Drop a plugin's catalogue. * * Removes the plugin's own directory, never the publisher's: `officerdev/` holds every first-party * plugin's, and taking it out because one was uninstalled would strip the rest. * * The codes then stop resolving, which is a defined state rather than a broken one — a device holding a * queued push for a departed plugin renders the unknown-code fallback. See docs/push-notifications.md. * * Safe to call when nothing was ever published. */ export async function unpublishNotifications(plugin: DiscoveredPlugin): Promise { await rm(notificationDirFor(plugin), { recursive: true, force: true }); }