OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / lib / commands / names.js

223 lines · javascript ref:
/**
 * 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)')
}