/**
* Release-registry subscriptions — persistent list of registry keys the
* local pgh-sidecar should replicate as live warm replicas.
*
* Without this, cross-machine OTA is broken: a subscriber has an empty
* own-registry, the `releases-latest` handler returns null, the daemon's
* OTA poller never sees new releases from the publisher.
*
* Config format (JSON at ~/.config/pgh/subscriptions.json):
* {
* "registries": ["<64-char hex>", ...], // release registries (OTA)
* "nameRegistries": ["<64-char hex>", ...] // NameRegistry autobases
* }
*
* sidecar reads this on boot, pgh install auto-adds, user can edit.
*
* `nameRegistries` is the NameRegistry analogue: a fresh standalone peer's
* sidecar boots its own empty NameRegistry, so it can't resolve bare app
* names claimed in someone else's registry. Subscribing to that registry's
* autobase key opens a read-only warm replica, so `pgh app install <name>`
* resolves. `pgh names subscribe <key>` adds one here.
*/
import process from '#node:process'
import fs from '#node:fs'
import path from '#node:path'
import { homedir } from '#node:os'
export const SUBSCRIPTIONS_PATH = path.join(homedir(), '.config', 'pgh', 'subscriptions.json')
// Resolved per call (not once at import) so tests and an isolated sidecar can
// point at their own file. Mirrors the PGH_SIDECAR_* overrides in
// pgh-sidecar/bin — subscriptions.json was the one piece of that isolation
// story with no override, so an experimental sidecar shared prod's file.
function subscriptionsPath () {
return process.env.PGH_SUBSCRIPTIONS_PATH || SUBSCRIPTIONS_PATH
}
export function readSubscriptions () {
try {
const raw = fs.readFileSync(subscriptionsPath(), 'utf8')
const j = JSON.parse(raw)
return Array.isArray(j.registries) ? j.registries.filter(k => typeof k === 'string' && k.length === 64) : []
} catch { return [] }
}
function writeSubscriptions (keys) {
// Preserve sibling keys (e.g. nameRegistries) — don't clobber the file.
const j = (() => { try { return JSON.parse(fs.readFileSync(subscriptionsPath(), 'utf8')) || {} } catch { return {} } })()
j.registries = [...new Set(keys)]
fs.mkdirSync(path.dirname(subscriptionsPath()), { recursive: true })
fs.writeFileSync(subscriptionsPath(), JSON.stringify(j, null, 2))
}
export function addSubscription (keyHex) {
if (!keyHex || keyHex.length !== 64) return false
const cur = readSubscriptions()
if (cur.includes(keyHex)) return false
cur.push(keyHex)
writeSubscriptions(cur)
return true
}
export function removeSubscription (keyHex) {
const cur = readSubscriptions()
const next = cur.filter(k => k !== keyHex)
if (next.length === cur.length) return false
writeSubscriptions(next)
return true
}
// ── Name-registry subscriptions ──────────────────────────────
// Same file, separate `nameRegistries` array. Read by the sidecar on boot
// to open read-only NameRegistry replicas (so bare app-name resolution
// works across publishers).
function readRaw () {
try { return JSON.parse(fs.readFileSync(subscriptionsPath(), 'utf8')) || {} }
catch { return {} }
}
function writeNameSubscriptions (keys) {
const j = readRaw()
j.nameRegistries = [...new Set(keys)]
fs.mkdirSync(path.dirname(subscriptionsPath()), { recursive: true })
fs.writeFileSync(subscriptionsPath(), JSON.stringify(j, null, 2))
}
export function readNameSubscriptions () {
const j = readRaw()
return Array.isArray(j.nameRegistries)
? j.nameRegistries.filter(k => typeof k === 'string' && k.length === 64)
: []
}
export function addNameSubscription (keyHex) {
if (!keyHex || keyHex.length !== 64) return false
const cur = readNameSubscriptions()
if (cur.includes(keyHex)) return false
cur.push(keyHex)
writeNameSubscriptions(cur)
return true
}
export function removeNameSubscription (keyHex) {
const cur = readNameSubscriptions()
const next = cur.filter(k => k !== keyHex)
if (next.length === cur.length) return false
writeNameSubscriptions(next)
return true
}