/** pgh releases — inspect the local ReleaseRegistry */
import process from '#node:process'
import fs from '#node:fs'
import path from '#node:path'
import { homedir } from '#node:os'
import Corestore from 'corestore'
import { Client } from 'hyper-svc'
import { isAbstract } from '../daemon-client.js'
export const usage = `pgh releases — inspect the local ReleaseRegistry
pgh releases list [name] List releases (all or one app)
pgh releases latest <name> Latest release of an app
pgh releases show <name> <version> A specific release
pgh releases key Print the registry core key
pgh releases remove <name> [version] Un-register an app (retire). Drops it
from the local conflict index and the
replicated release registry. No version
= remove ALL versions of the app.`
import { ReleaseRegistry } from '../release-registry.js'
import { PublishedIndex } from '../published-index.js'
const DEFAULT_STORE = path.join(homedir(), '.config', 'pgh', 'release-registry')
const SIDECAR_SOCK = process.env.PGH_SIDECAR_SOCK || path.join(homedir(), '.pgh', 'svc.sock')
// Prefer pgh-sidecar (which owns the registry lock). Fall back to
// opening the corestore directly when the sidecar isn't running.
export async function withRegistry (fn) {
if (isAbstract(SIDECAR_SOCK) || fs.existsSync(SIDECAR_SOCK)) {
let client = null
try {
client = await Client.connectUds(SIDECAR_SOCK)
return await fn(sidecarRegistryAdapter(client))
} finally {
try { if (client) await client.close() } catch {}
}
}
fs.mkdirSync(DEFAULT_STORE, { recursive: true })
const store = new Corestore(DEFAULT_STORE)
const reg = new ReleaseRegistry(store)
await reg.ready()
try {
return await fn(reg)
} finally {
await reg.close()
await store.close()
}
}
// Minimal adapter that mirrors the ReleaseRegistry methods the
// commands below call (keyHex, list, latest, get), implemented over
// the sidecar's UDS RPCs.
function sidecarRegistryAdapter (client) {
return {
get keyHex () { return this._keyHex },
async _loadKey () {
if (this._keyHex) return
const info = await client.call('registry-info', {})
this._keyHex = info.keyHex
},
async list (name) {
return client.call('releases-list', name ? { name } : {})
},
async latest (name) {
return client.call('releases-latest', { name })
},
async get (name, version) {
return client.call('releases-get', { name, version })
},
async remove (name, version) {
return client.call('remove-release', { name, version })
},
// Not a registry method: the sidecar runs release-retention gc itself,
// because `list()` of every release overflows the UDS frame.
async storeGc (opts) {
return client.call('store-gc', opts)
}
}
}
export async function run (client, args) {
const sub = args[0] || 'list'
if (sub === 'help') {
return usage
}
if (sub === 'key') {
return withRegistry(async (reg) => {
if (typeof reg._loadKey === 'function') await reg._loadKey()
return { keyHex: reg.keyHex }
})
}
if (sub === 'list' || sub === 'ls') {
const nameFilter = args[1] || null
return withRegistry(async (reg) => {
const entries = await reg.list(nameFilter)
return entries.map(e => ({
name: e.name,
version: e.version,
treeHash: e.treeHash.slice(0, 12),
publishedAt: e.publishedAt,
signed: !!e.sig
}))
})
}
if (sub === 'latest') {
const name = args[1]
if (!name) throw new Error('usage: pgh releases latest <name>')
return withRegistry(async (reg) => {
const entry = await reg.latest(name)
if (!entry) throw new Error('no releases for ' + name)
return entry
})
}
if (sub === 'show') {
const name = args[1]
const version = args[2]
if (!name || !version) throw new Error('usage: pgh releases show <name> <version>')
return withRegistry(async (reg) => {
const entry = await reg.get(name, version)
if (!entry) throw new Error('no release ' + name + '@' + version)
return entry
})
}
if (sub === 'remove' || sub === 'retire') {
const name = args[1]
if (!name) throw new Error('usage: pgh releases remove <name> [version]')
const version = args[2] || null
const result = { name, version: version || 'all', publishedIndex: false, releaseRegistry: 0, registryNote: null }
// 1. Local conflict index — what `pgh publish` compares against. Removing
// here is what actually drops the app out of conflict detection.
const index = new PublishedIndex()
result.publishedIndex = index.remove(name, version)
// 2. Replicated release registry — un-advertise so it's no longer
// installable. Best-effort: needs the sidecar's remove-release RPC
// (or direct access when the sidecar is down). Degrade gracefully so
// the local removal above still stands.
try {
await withRegistry(async (reg) => {
if (version) { await reg.remove(name, version); result.releaseRegistry = 1 }
else {
const entries = await reg.list(name)
for (const e of entries) { await reg.remove(e.name, e.version); result.releaseRegistry++ }
}
})
} catch (err) {
result.registryNote = 'release-registry not updated (' + (err.message || err) +
') — local conflict index removal still applied. Update pgh-sidecar for full un-register.'
}
return result
}
throw new Error('unknown releases subcommand: ' + sub + '\n' + usage)
}