OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / lib / commands / user.js

115 lines · javascript ref:
/**
 * pgh user — manage users on a pgh-managed app.
 *
 * Subcommands:
 *   pgh user list   --app <name>
 *   pgh user promote <identity-hex> --app <name>
 *   pgh user revoke  <identity-hex> --app <name>
 *
 * Dispatches via pgh-sidecar's app registry: looks up the running app's
 * UDS by name, then calls the app's RPCs (list-users / grant-permission /
 * revoke-identity). The app must be running and registered with sidecar.
 */

import process from '#node:process'
import { isAbstract } from '../daemon-client.js'
import fs from '#node:fs'
import path from '#node:path'
import { homedir } from '#node:os'

const SIDECAR_SOCK = process.env.PGH_SIDECAR_SOCK || path.join(homedir(), '.pgh', 'svc.sock')

export const usage = `pgh user — manage users on a pgh-managed app

  pgh user list --app <name>
  pgh user promote <identity-hex> --app <name>
  pgh user revoke <identity-hex> --app <name>

The app must be running and registered with pgh-sidecar.`

async function appClient (appName) {
  if (!isAbstract(SIDECAR_SOCK) && !fs.existsSync(SIDECAR_SOCK)) {
    throw new Error('pgh-sidecar is not running (no socket at ' + SIDECAR_SOCK + ')')
  }
  const { Client } = await import('hyper-svc')
  const sc = await Client.connectUds(SIDECAR_SOCK)
  let entry
  try {
    entry = await sc.call('app-lookup', { name: appName })
  } finally {
    try { await sc.close() } catch {}
  }
  if (!entry || !entry.found) {
    throw new Error(`app '${appName}' not registered with pgh-sidecar (is it running?)`)
  }
  return Client.connectUds(entry.udsPath)
}

function parseAppFlag (args) {
  const i = args.indexOf('--app')
  if (i === -1 || !args[i + 1]) {
    throw new Error('--app <name> is required (the pgh-managed app to talk to)')
  }
  return args[i + 1]
}

export async function run (_client, args /*, globals */) {
  const sub = args[0]

  if (!sub || sub === 'help') {
    return [
      'usage: pgh user <subcommand> [options]',
      '  list                                 List users (--app required)',
      '  promote <identity-hex>               Grant admin globally',
      '  revoke  <identity-hex>               Revoke a linked identity',
      '',
      'Common: --app <name>   The pgh-managed app to talk to (e.g. pear-git-v3)'
    ].join('\n')
  }

  if (sub === 'list') {
    const app = parseAppFlag(args)
    const c = await appClient(app)
    try {
      const users = await c.call('list-users', {})
      return users
    } finally { try { await c.close() } catch {} }
  }

  if (sub === 'promote') {
    const identity = args[1]
    if (!identity || identity.length !== 64) {
      throw new Error('usage: pgh user promote <identity-hex> --app <name>')
    }
    const app = parseAppFlag(args)
    const c = await appClient(app)
    try {
      const updated = await c.call('grant-permission', {
        identity,
        permissions: ['admin']
      })
      return {
        promoted: identity,
        userId: updated.userId,
        name: updated.name || null,
        globalPermissions: updated.globalPermissions || []
      }
    } finally { try { await c.close() } catch {} }
  }

  if (sub === 'revoke') {
    const identity = args[1]
    if (!identity || identity.length !== 64) {
      throw new Error('usage: pgh user revoke <identity-hex> --app <name>')
    }
    const app = parseAppFlag(args)
    const c = await appClient(app)
    try {
      const r = await c.call('revoke-identity', { identityId: identity })
      return r
    } finally { try { await c.close() } catch {} }
  }

  throw new Error('unknown user subcommand: ' + sub)
}