/**
* pgh names / pgh claim / pgh release — NameRegistry CLI surface.
*
* All operations route through pgh-sidecar's UDS. The sidecar owns the
* NameRegistry corestore (single writer) and advertises it on the
* configured NameRegistry topic over Hyperswarm.
*
* Usage:
* pgh claim <name> [--description "..."] [--registry-key <hex>]
* pgh release <name>
* pgh names list [--publisher <identityRoot>] [--include-released]
* pgh names resolve <name>
*/
import process from '#node:process'
import path from '#node:path'
import { homedir } from '#node:os'
import fs from '#node:fs'
import { Client } from 'hyper-svc'
import { isAbstract } from '../daemon-client.js'
const SIDECAR_SOCK = process.env.PGH_SIDECAR_SOCK || path.join(homedir(), '.pgh', 'svc.sock')
export const usage = `pgh names / claim / release — NameRegistry (sidecar-owned)
pgh claim <name> [--description "..."] [--registry-key <hex>]
pgh release <name> Tombstone a name you own
pgh names list [--publisher <id>] [--include-released]
pgh names resolve <name>
pgh names update <name> [--registry-key <hex>] [--server-key <hex>] [--description "..."]
Re-point a name you own (rotate registry/server key)
pgh names reanchor <name> | --all [--force]
Repair a claim anchored to a device key (org #142)
pgh names info
pgh names subscribe <registryKeyHex> Replicate another publisher's NameRegistry (read-only)
pgh names subscriptions List own + subscribed name registries`
async function withClient (fn) {
if (!isAbstract(SIDECAR_SOCK) && !fs.existsSync(SIDECAR_SOCK)) {
throw new Error('pgh-sidecar is not running (no socket at ' + SIDECAR_SOCK + ')')
}
const c = await Client.connectUds(SIDECAR_SOCK)
try {
return await fn(c)
} finally {
try { await c.close() } catch {}
}
}
function argVal (args, flag) {
const i = args.indexOf(flag)
return i !== -1 && args[i + 1] ? args[i + 1] : null
}
function hasFlag (args, flag) {
return args.indexOf(flag) !== -1
}
// ── Exposed as `pgh claim` ───────────────────────────────────
export async function runClaim (_client, args) {
const name = args[0]
if (!name) throw new Error('usage: pgh claim <name> [--description "..."] [--registry-key <hex>]')
const description = argVal(args, '--description') || ''
const registryKey = argVal(args, '--registry-key') || undefined
return withClient(async (c) => {
const entry = await c.call('claim-name', { name, description, registryKey })
if (!entry) throw new Error('claim did not surface — check sidecar writable status')
return {
claimed: entry.name,
publisher: entry.publisher,
description: entry.description,
claimedAt: entry.claimedAt
}
})
}
// ── Exposed as `pgh release` ─────────────────────────────────
export async function runRelease (_client, args) {
const name = args[0]
if (!name) throw new Error('usage: pgh release <name>')
return withClient(async (c) => {
return await c.call('release-name', { name })
})
}
// ── Exposed as `pgh names <sub>` ─────────────────────────────
export async function run (_client, args) {
const sub = args[0] || 'list'
if (sub === 'help') {
return [
'usage: pgh names <subcommand>',
' list [--publisher <identityRoot>] [--include-released]',
' resolve <name>',
' update <name> [--registry-key <hex>] [--server-key <hex>] [--description "..."]',
' Re-point a name you own',
' reanchor <name> | --all Move a pre-#142 claim from its device key to the identity root',
' info Print registry key + writable status',
' subscribe <registryKeyHex> Replicate another publisher\'s NameRegistry (read-only)',
' subscriptions List own + subscribed name registries',
'See also:',
' pgh claim <name> Register a new name for this publisher',
' pgh release <name> Tombstone a name you own'
].join('\n')
}
if (sub === 'info') {
return withClient(async (c) => c.call('name-registry-info', {}))
}
if (sub === 'list' || sub === 'ls') {
const publisher = argVal(args, '--publisher') || null
const includeReleased = hasFlag(args, '--include-released')
return withClient(async (c) => {
const list = await c.call('list-names', {
...(publisher ? { publisher } : {}),
includeReleased
})
return list.map(e => ({
name: e.name,
publisher: {
identityRoot: (e.publisher.identityRoot || '').slice(0, 12),
registryKey: (e.publisher.registryKey || '').slice(0, 12)
},
description: e.description,
claimedAt: e.claimedAt
}))
})
}
if (sub === 'resolve') {
const name = args[1]
if (!name) throw new Error('usage: pgh names resolve <name>')
return withClient(async (c) => {
const entry = await c.call('resolve-name', { name })
if (!entry) throw new Error('no claim for "' + name + '"')
return entry
})
}
if (sub === 'update') {
const name = args[1]
if (!name) {
throw new Error('usage: pgh names update <name> [--registry-key <hex>] [--server-key <hex>] [--description "..."]')
}
const registryKey = argVal(args, '--registry-key')
const serverKey = argVal(args, '--server-key')
const description = argVal(args, '--description')
if (!registryKey && !serverKey && description === null) {
throw new Error('nothing to update: pass --registry-key, --server-key and/or --description')
}
for (const [flag, val] of [['--registry-key', registryKey], ['--server-key', serverKey]]) {
if (val && val.length !== 64) throw new Error(flag + ' must be 64-char hex')
}
return withClient(async (c) => {
const params = { name }
if (registryKey) params.registryKey = registryKey
if (serverKey) params.serverKey = serverKey
if (description !== null) params.description = description
return await c.call('update-name', params)
})
}
if (sub === 'reanchor') {
const all = hasFlag(args, '--all')
// Re-emit even where this peer already shows the claim re-anchored: a peer
// that applied the op without an OP_REANCHOR_NAME branch consumed it, and
// upgrading does not re-run history. The op is idempotent per-peer.
const force = hasFlag(args, '--force')
const name = args[1] && args[1][0] !== '-' ? args[1] : null
if (!name && !all) {
throw new Error('usage: pgh names reanchor <name> | pgh names reanchor --all [--force]')
}
return withClient(async (c) => {
const targets = name ? [name] : (await c.call('list-names', { own: true })).map(e => e.name)
const out = []
for (const n of targets) {
try {
const res = await c.call('reanchor-name', { name: n, force })
out.push({
name: n,
status: res.alreadyAnchored ? 'already anchored' : (res.forced ? 're-emitted (for peers behind)' : 're-anchored'),
identityRoot: res.entry.publisher.identityRoot
})
} catch (err) {
// One name that cannot be repaired must not abort the sweep — the
// registry may hold claims from other devices entirely.
out.push({ name: n, status: 'skipped: ' + err.message })
}
}
return out
})
}
if (sub === 'subscribe') {
const key = args[1]
if (!key || key.length !== 64) {
throw new Error('usage: pgh names subscribe <registryKeyHex> (64-char hex; see `pgh names info` on the owner)')
}
return withClient(async (c) => {
const r = await c.call('subscribe-name-registry', { registryKey: key })
return {
subscribed: key.slice(0, 12) + '…',
alreadyOwn: !!r.alreadyOwn,
opened: !!r.opened,
persisted: !!r.persisted,
replicas: (r.subscriptions || []).map(k => k.slice(0, 12) + '…')
}
})
}
if (sub === 'subscriptions' || sub === 'subs') {
return withClient(async (c) => c.call('name-subscriptions', {}))
}
throw new Error('unknown names subcommand: ' + sub + ' (try: list, resolve, info, subscribe, subscriptions, help)')
}