OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / lib / daemon-client.js

56 lines · javascript ref:
/**
 * Shared helper: resolve a pgh-managed app's daemon UDS via pgh-sidecar's
 * app-lookup, then return a hyper-svc Client connected to that UDS.
 *
 * Used by all the daemon-RPC commands ported in from
 * pear-git-v3/lib/pgh/commands/* during the CLI consolidation (see
 * pear-git-v3 README for the architectural plan).
 */

import fs from '#node:fs'
import path from '#node:path'
import { homedir } from '#node:os'
import process from '#node:process'

// Honour PGH_SIDECAR_SOCK so an experimental sidecar at a non-default
// path (~/.pgh-experimental/svc.sock) is reachable without patching
// every command. Falls back to the canonical ~/.pgh/svc.sock.
const SIDECAR_SOCK = process.env.PGH_SIDECAR_SOCK ||
  path.join(homedir(), '.pgh', 'svc.sock')

// An '@'-prefixed socket is a Linux abstract-namespace socket (no filesystem
// entry), used where SELinux denies pathname sockets (unrooted Android/celltron).
export function isAbstract (p) {
  return typeof p === 'string' && p.length > 1 && p[0] === '@'
}

export async function daemonClient (appName = 'pear-git-v3') {
  // The existence check only applies to pathname sockets; an abstract socket has
  // no fs entry, so fs.existsSync would always be false and wrongly report the
  // sidecar as down. hyper-svc's Client handles the '@' at connect time.
  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)
}

/**
 * Run an async fn with a daemon client, ensuring the client is closed
 * even on error. Most ported commands take a client arg from their
 * dispatcher; this helper auto-resolves one when called from base pgh's
 * dispatcher (which passes null as the client).
 */
export async function withDaemon (appName, fn) {
  const client = await daemonClient(appName)
  try { return await fn(client) }
  finally { try { await client.close() } catch {} }
}