/**
* pgh CLI — single command dispatcher for the entire pgh stack.
*
* Two flavors of commands:
* - Pure-local (init, store, publish, releases, app, seed, names,
* user, git-passthrough): no daemon client; operate on filesystem
* + local registries.
* - Daemon-RPC (status, dump-keys, repo, issue, org, profile,
* search, deploy, label, admin, config): receive a hyper-svc
* Client connected to the named pgh-managed app's UDS (default:
* pear-git-v3) via pgh-sidecar's app-lookup.
*
* Architecture: one CLI binary, one set of commands. Apps run as
* pgh-managed daemons; pgh CLI is the only client. Phase 2 of the
* consolidation folded pear-git-v3's previous duplicate `bin/pgh` +
* `lib/pgh/commands/` into this dispatcher.
*/
import process from '#node:process'
import { formatOutput } from './output.js'
import { daemonClient } from './daemon-client.js'
const COMMANDS = {
// Pure-local (no daemon)
init: () => import('./commands/init.js'),
store: () => import('./commands/store.js'),
publish: () => import('./commands/publish.js'),
releases: () => import('./commands/releases.js'),
app: () => import('./commands/app.js'),
seed: () => import('./commands/seed.js'),
names: () => import('./commands/names.js'),
claim: () => import('./commands/names.js').then(m => ({ run: m.runClaim, usage: m.usage })),
release: () => import('./commands/names.js').then(m => ({ run: m.runRelease, usage: m.usage })),
user: () => import('./commands/user.js'),
overlay: () => import('./commands/overlay.js'),
// Mixed (some subcommands talk to daemon, some don't — handled inside)
identity: () => import('./commands/identity.js'),
device: () => import('./commands/device.js'),
peer: () => import('./commands/peer.js'),
// Daemon-RPC (auto-connect to pear-git-v3 daemon, override with --app)
status: () => import('./commands/status.js'),
'dump-keys': () => import('./commands/dump-keys.js'),
search: () => import('./commands/search.js'),
profile: () => import('./commands/profile.js'),
token: () => import('./commands/token.js'),
attach: () => import('./commands/attach.js'),
deploy: () => import('./commands/deploy.js'),
label: () => import('./commands/label.js'),
'ssh-key': () => import('./commands/ssh-key.js'),
admin: () => import('./commands/admin.js'),
repo: () => import('./commands/repo.js'),
issue: () => import('./commands/issue.js'),
org: () => import('./commands/org.js'),
config: () => import('./commands/config.js'),
// Git passthrough (pure local; commands auto-detect pear remote)
commit: () => import('./commands/commit.js'),
clone: () => import('./commands/clone.js'),
pull: () => import('./commands/pull.js'),
push: () => import('./commands/push.js'),
remote: () => import('./commands/remote.js')
}
// Commands that auto-receive a daemon client. Others get null.
// `clone` is here too: clone.js calls get-repo to resolve the repo's
// locally-materialized bare path before cloning from it.
const NEEDS_DAEMON = new Set([
'status', 'dump-keys', 'search', 'profile', 'token', 'attach', 'deploy', 'label',
'ssh-key', 'admin', 'repo', 'issue', 'org', 'config', 'clone'
])
// Canonical pgh: daemon commands target the canonical pear-git-v3 daemon by
// default (matches the help text + the post-cutover canonical stack). Override
// with --app or PGH_DEFAULT_APP (e.g. PGH_DEFAULT_APP=pear-git-v3-experimental
// for the experimental daemon).
const DEFAULT_APP = process.env.PGH_DEFAULT_APP || 'pear-git-v3'
// Git-passthrough commands forward their args (including --help/-h) to git, so
// `pgh push main --help` should reach git. We only show pgh's own usage for
// these when help is the FIRST token (`pgh push --help`).
const PASSTHROUGH = new Set(['commit', 'push', 'pull', 'remote', 'clone'])
export async function run (argv) {
const { globals, command, args } = parseArgv(argv)
if (!command || command === 'help' || command === '--help' || command === '-h') {
printUsage()
return
}
const loader = COMMANDS[command]
if (!loader) {
console.error('pgh: unknown command "' + command + '"')
console.error('Run "pgh help" for usage.')
process.exit(1)
}
const mod = await loader()
// Per-command help. Handled BEFORE the daemon connection below — otherwise
// `pgh issue --help` (a NEEDS_DAEMON command) would fail with a connection
// error instead of printing usage when no daemon is reachable.
if (wantsHelp(command, args)) {
printCommandUsage(command, mod)
return
}
// Past the help check, `--` has done its job — hand commands the values.
const cmdArgs = stripOptionTerminator(command, args)
const appName = globals.app || DEFAULT_APP
let client = null
if (NEEDS_DAEMON.has(command)) {
try { client = await daemonClient(appName) }
catch (err) {
console.error('pgh: ' + err.message)
if (globals.debug) console.error(err.stack)
process.exit(1)
}
}
try {
const result = await mod.run(client, cmdArgs, globals)
if (result !== undefined && result !== null) {
console.log(formatOutput(result, globals.format))
}
} catch (err) {
console.error('pgh: ' + (err.message || err))
if (globals.debug) console.error(err.stack)
process.exit(1)
} finally {
if (client) try { await client.close() } catch {}
}
}
// True when the user is asking for a command's usage rather than running it.
// For passthrough commands we only treat a LEADING --help/-h/help as ours
// (anything later is forwarded to git); for everything else, --help/-h
// anywhere triggers usage.
export function wantsHelp (command, args) {
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return true
if (PASSTHROUGH.has(command)) return false
// `--` ends option parsing (POSIX convention): everything after it is a
// VALUE, never a flag. Without this there is no way to name a thing that
// looks like a flag — and the forge really does contain a repo called
// `--help`, created back when a read minted repos. `pgh repo retire --help`
// rightly prints usage; `pgh repo retire -- --help` retires that repo.
const end = args.indexOf('--')
const flagArgs = end === -1 ? args : args.slice(0, end)
return flagArgs.includes('--help') || flagArgs.includes('-h')
}
/**
* Drop the first standalone `--` so commands receive the real values.
*
* NOT applied to passthrough commands: those forward argv to git, where `--`
* is meaningful (`git checkout -- <path>`) and must survive untouched.
*/
export function stripOptionTerminator (command, args) {
if (PASSTHROUGH.has(command)) return args
const i = args.indexOf('--')
return i === -1 ? args : args.slice(0, i).concat(args.slice(i + 1))
}
function printCommandUsage (command, mod) {
const u = typeof mod.usage === 'function' ? mod.usage() : mod.usage
if (u) {
console.log(String(u).replace(/\n+$/, ''))
return
}
console.log('pgh ' + command + ': no detailed help available.')
console.log('Run "pgh help" for the full command list.')
}
function parseArgv (argv) {
const globals = { format: 'table', debug: false }
const rest = []
let sawCommand = false
// Globals are ONLY consumed before the command. Once we see a non-flag
// token (the command), any later --json / --format / --debug is passed
// through as an arg to the subcommand. Prevents `peer call ... --json
// '{...}'` from having its --json swallowed as a format flag.
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (!sawCommand) {
if (a === '--format' && argv[i + 1]) { globals.format = argv[++i]; continue }
if (a === '--json') { globals.format = 'json'; continue }
if (a === '--debug') { globals.debug = true; continue }
if (a === '--app' && argv[i + 1]) { globals.app = argv[++i]; continue }
if (!a.startsWith('--')) sawCommand = true
}
rest.push(a)
}
return { globals, command: rest[0] || null, args: rest.slice(1) }
}
function printUsage () {
console.log(`pgh — P2P content-addressed package manager
Usage: pgh [--json] [--debug] <command> [args...]
Commands:
init [--bootstrap <url>] One-time setup on this machine
app install|update|remove|list App install/update/uninstall
app start|stop|restart|status Process lifecycle (supervisor-managed)
app log <name> [-f] [--lines N] Tail app.log
app autostart <name> on|off Mark app for inclusion in 'app boot'
app boot | shutdown Start/stop all autostart apps in dep order
app adopt <path> [--name <name>] Register a source-tree dir as an app
app gc <name> Run 'git gc --prune=now' on each bare repo in the app's repos dir
app heal [name] fsck every bare repo; refetch or force-reclone any with missing objects. No name = all apps.
store list|size|gc|pin|unpin Tree store management
publish [dir] Compute lockfile + record release
seed <dir> [--topic <hex>] Serve a published app on Hyperswarm
releases list|latest|show Inspect the local ReleaseRegistry
identity show Print root/device pubkey + chain depth
device invite [--label <name>] Generate link-invite for a second device
device join <inviteHex> Accept an invite, write linked-device
device list --peer <url> List devices linked to your identity on <url>
device revoke <pubkey> --peer <url> Revoke a linked device on <url>
device label <pubkey> <name> --peer <url> Rename a linked device on <url>
peer call <url> <method> [--param k=v] [--json <JSON>]
Envelope-signed raw RPC to a remote daemon
claim <name> [--description "..."] Register a flat app name (NameRegistry)
release <name> Tombstone a name you own
names list|resolve|info Inspect the NameRegistry
Daemon commands (target an app's daemon; default pear-git-v3, override --app <name>):
status Daemon status (mounts, swarm, counts)
repo list|show|files|commits|... Browse repos served by the daemon
repo invite|join Repo-scoped P2P pairing (peer mode)
issue <repo>|--org <n> [--create] List/create/view issues (--close|--comment|...)
org list|show|create|delete Orgs and membership
org invite|join Org-scoped P2P pairing (peer mode)
org add-resource|remove-resource Attach/detach a repo to/from an org
profile view|set Identity profile
search repo|code <query> Search repos / code
label ... Issue labels
deploy setup|trigger|status|log CI/CD config + release log
admin ... Admin operations
config list|set Local CLI config
dump-keys Bootstrap keys (UDS only)
Git passthrough (auto-detects a pear:// / punch:// remote):
clone <repo> Clone a daemon-served repo
commit | push | pull git passthrough over a pear remote
remote add|rm|list|migrate-origins Manage pear remotes (NameRegistry petnames)
Other:
user list|promote|revoke --app <n> Manage users on an app's daemon
overlay ... hyper-overlay file routing
help | --help | -h This message
Flags:
--app <name> Target a specific app's daemon (default pear-git-v3)
--json Output as JSON
--debug Print stack traces on error
--format table|json|plain Output format`)
}