OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / bin / pgh

312 lines · text ref:
#!/usr/bin/env bare
await import('bare-node-runtime/global')

/**
 * pgh - Punch Git Hub CLI
 *
 * A CLI for pear-git, parallel to GitHub's gh.
 *
 * Usage:
 *   pgh <command> <subcommand> [args] [--flags]
 */

import { PghClient } from '../lib/cli/client.js'
import { loadConfig, saveConfig, hasDeviceIdentity } from '../lib/cli/config.js'
import { log, error } from '../lib/cli/output.js'
import { repoCommand, repoHelp } from '../lib/cli/commands/repo.js'
import { issueCommand, issueHelp } from '../lib/cli/commands/issue.js'
import { labelCommand, labelHelp } from '../lib/cli/commands/label.js'
import { searchCommand, searchHelp } from '../lib/cli/commands/search.js'
import { authCommand, authHelp } from '../lib/cli/commands/auth.js'
import { tokenCommand, tokenHelp } from '../lib/cli/commands/token.js'
import { remoteCommand, remoteHelp } from '../lib/cli/commands/remote.js'
import { profileCommand, profileHelp } from '../lib/cli/commands/profile.js'
import { serveCommand, serveHelp } from '../lib/cli/commands/serve.js'
import { adminCommand, adminHelp } from '../lib/cli/commands/admin.js'
import { identityCommand, identityHelp } from '../lib/cli/commands/identity.js'
import { peerCommand, peerHelp } from '../lib/cli/commands/peer.js'
import { pushCommand, pushHelp } from '../lib/cli/commands/push.js'
import { pullCommand, pullHelp } from '../lib/cli/commands/pull.js'
import { commitCommand, commitHelp } from '../lib/cli/commands/commit.js'
import { deployCommand, deployHelp } from '../lib/cli/commands/deploy.js'
import { groupCommand, groupHelp } from '../lib/cli/commands/group.js'
import { registryCommand, registryHelp, federationCommand, federationHelp, accessCommand, accessHelp } from '../lib/cli/commands/registry.js'
import { appCommand, appHelp } from '../lib/cli/commands/app.js'
import { serverCommand, serverHelp } from '../lib/cli/commands/server.js'
import { gitSpawn } from '../lib/cli/git.js'

const VERSION = '0.1.0'

// Parse args — globalThis.__pghArgs is set when invoked via `pear run ... pgh`
const rawArgs = globalThis.__pghArgs || process.argv.slice(2)
const flags = {}
const positional = []

// Boolean flags that never take a value argument
const booleanFlags = new Set([
  '--json', '--help', '-h', '--version', '-v', '--verbose',
  '--force', '--rebase', '--recurse-submodules', '--set-upstream', '-u',
  '-a', '--all', '--amend', '--dry-run',
  '--mirror', '--peer'
])

for (let i = 0; i < rawArgs.length; i++) {
  const arg = rawArgs[i]
  if (booleanFlags.has(arg)) {
    const key = arg.startsWith('--') ? arg.slice(2) : arg.slice(1)
    flags[key] = true
  } else if (arg.startsWith('--') && rawArgs[i + 1] && !rawArgs[i + 1].startsWith('--')) {
    flags[arg.slice(2)] = rawArgs[++i]
  } else if (arg.startsWith('-') && arg.length === 2 && rawArgs[i + 1] && !rawArgs[i + 1].startsWith('-')) {
    flags[arg.slice(1)] = rawArgs[++i]
  } else if (!arg.startsWith('-')) {
    positional.push(arg)
  }
}

const command = positional[0]
const subcommand = positional[1]
const args = positional.slice(2)

if (flags.version) {
  log(`pgh ${VERSION}`)
  process.exit(0)
}

if (!command || flags.help) {
  if (command === 'repo') { repoHelp(); process.exit(0) }
  if (command === 'issue') { issueHelp(); process.exit(0) }
  if (command === 'label') { labelHelp(); process.exit(0) }
  if (command === 'search') { searchHelp(); process.exit(0) }
  if (command === 'auth') { authHelp(); process.exit(0) }
  if (command === 'token') { tokenHelp(); process.exit(0) }
  if (command === 'remote') { remoteHelp(); process.exit(0) }
  if (command === 'profile') { profileHelp(); process.exit(0) }
  if (command === 'serve') { serveHelp(); process.exit(0) }
  if (command === 'admin') { adminHelp(); process.exit(0) }
  if (command === 'identity') { identityHelp(); process.exit(0) }
  if (command === 'peer') { peerHelp(); process.exit(0) }
  if (command === 'push') { pushHelp(); process.exit(0) }
  if (command === 'pull') { pullHelp(); process.exit(0) }
  if (command === 'commit') { commitHelp(); process.exit(0) }
  if (command === 'deploy') { deployHelp(); process.exit(0) }
  if (command === 'group') { groupHelp(); process.exit(0) }
  if (command === 'registry') { registryHelp(); process.exit(0) }
  if (command === 'federation') { federationHelp(); process.exit(0) }
  if (command === 'access') { accessHelp(); process.exit(0) }
  if (command === 'app') { appHelp(); process.exit(0) }
  if (command === 'server') { serverHelp(); process.exit(0) }
  showHelp()
  process.exit(0)
}

// First-run guard — require device identity before most commands
const skipGuardCommands = new Set(['identity', 'config', 'help', 'version', 'install', 'serve', 'app', 'server', 'restart'])
if (command && !skipGuardCommands.has(command) && !hasDeviceIdentity()) {
  log('pgh is not configured. Run: pgh identity setup')
  process.exit(1)
}

const client = new PghClient(flags)

try {
  switch (command) {
    case 'repo':
      await repoCommand(client, subcommand, args, flags)
      break
    case 'issue':
      await issueCommand(client, subcommand, args, flags)
      break
    case 'label':
      await labelCommand(client, subcommand, args, flags)
      break
    case 'search':
      await searchCommand(client, subcommand, args, flags)
      break
    case 'auth':
      await authCommand(client, subcommand, args, flags)
      break
    case 'token':
      await tokenCommand(client, subcommand, args, flags)
      break
    case 'remote':
      await remoteCommand(client, subcommand, args, flags)
      break
    case 'profile':
      await profileCommand(client, subcommand, args, flags)
      break
    case 'serve':
      await serveCommand(client, subcommand, args, flags)
      break
    case 'admin':
      await adminCommand(client, subcommand, args, flags)
      break
    case 'identity':
      await identityCommand(client, subcommand, args, flags)
      break
    case 'peer':
      await peerCommand(client, subcommand, args, flags)
      break
    case 'push':
      await pushCommand(client, subcommand, args, flags)
      break
    case 'pull':
      await pullCommand(client, subcommand, args, flags)
      break
    case 'commit':
      await commitCommand(client, subcommand, args, flags)
      break
    case 'deploy':
      await deployCommand(client, subcommand, args, flags)
      break
    case 'group':
      await groupCommand(client, subcommand, args, flags)
      break
    case 'registry':
      await registryCommand(client, subcommand, args, flags)
      break
    case 'federation':
      await federationCommand(client, subcommand, args, flags)
      break
    case 'access':
      await accessCommand(client, subcommand, args, flags)
      break
    case 'app':
      await appCommand(client, subcommand, args, flags)
      break
    case 'server':
      await serverCommand(client, subcommand, args, flags)
      break

    case 'config': {
      if (subcommand === 'set' && args[0] && args[1]) {
        const cfg = loadConfig()
        cfg[args[0]] = args[1]
        saveConfig(cfg)
        log(`Set ${args[0]} = ${args[1]}`)
      } else if (subcommand === 'get' && args[0]) {
        const cfg = loadConfig()
        log(cfg[args[0]] ?? '(not set)')
      } else if (subcommand === 'list' || !subcommand) {
        const cfg = loadConfig()
        for (const [k, v] of Object.entries(cfg)) {
          log(`${k} = ${v ?? '(not set)'}`)
        }
      } else {
        log('Usage: pgh config [set <key> <value> | get <key> | list]')
      }
      break
    }

    case 'help':
      if (subcommand === 'repo') { repoHelp(); break }
      if (subcommand === 'issue') { issueHelp(); break }
      if (subcommand === 'label') { labelHelp(); break }
      if (subcommand === 'search') { searchHelp(); break }
      if (subcommand === 'auth') { authHelp(); break }
      if (subcommand === 'token') { tokenHelp(); break }
      if (subcommand === 'remote') { remoteHelp(); break }
      if (subcommand === 'profile') { profileHelp(); break }
      if (subcommand === 'serve') { serveHelp(); break }
      if (subcommand === 'admin') { adminHelp(); break }
      if (subcommand === 'identity') { identityHelp(); break }
      if (subcommand === 'peer') { peerHelp(); break }
      if (subcommand === 'push') { pushHelp(); break }
      if (subcommand === 'pull') { pullHelp(); break }
      if (subcommand === 'commit') { commitHelp(); break }
      if (subcommand === 'deploy') { deployHelp(); break }
      if (subcommand === 'group') { groupHelp(); break }
      if (subcommand === 'registry') { registryHelp(); break }
      if (subcommand === 'federation') { federationHelp(); break }
      if (subcommand === 'access') { accessHelp(); break }
      if (subcommand === 'app') { appHelp(); break }
      if (subcommand === 'server') { serverHelp(); break }
      showHelp()
      break

    case 'version':
      log(`pgh ${VERSION}`)
      break

    case 'restart':
      await serveCommand(client, 'restart', args, flags)
      break

    default:
      // Pass through to git for any unrecognized command
      await gitSpawn(rawArgs, '.')
  }
} catch (err) {
  if (err.message.includes('exited with code')) {
    // Git passthrough error — exit code is in the message
    const match = err.message.match(/code (\d+)/)
    process.exit(match ? parseInt(match[1], 10) : 1)
  }
  error(err.message)
}

function showHelp () {
  log(`pgh - Punch Git Hub CLI for pear-git

Usage:
  pgh <command> <subcommand> [args] [--flags]

Git:
  commit    Stage and commit changes
  push      Push to pear remote (auto-detected)
  pull      Pull from pear remote (auto-detected)
  remote    Petname remotes (add, rm, list, rename, migrate-origins)
  <git cmd> Any unrecognized command is passed through to git

Repos:
  repo      Manage repositories (list, info, clone, create, scan, import, link)
  search    Search repos and code (repo, code)
  deploy    Pear app deployment (setup, trigger, status, log, disable)

Issues:
  issue     Manage issues (list, create, view, close, reopen, comment)
  label     Manage labels (list, create, update, delete)

Identity:
  identity  Device identity (setup, status, show, link, export, change-password)
  auth      Authentication (status, whoami, test)
  token     Token management (create, list, revoke, test)
  profile   User profile (view, set)

Server:
  serve        Server management (status, p2p, restart, shutdown)
  server       Server identity (export-mnemonic, link, join, status)
  admin        Admin tools (users, identities, challenges, merge, stats)
  group        Manage namespace groups (create, list, info, add-repo, members)
  registry     P2P namespace registry (share, join, invite, status, peers)
  federation   Server-to-server linking (invite, join, peers, revoke)
  access       Access grants (grants, invites, grant-group, grant-repo, join, revoke)
  peer         Server peering + P2P access (add, join, servers, repair, invite, accept)
  config       Manage config (set, get, list)

Apps:
  app          Install and manage Pear apps locally (install, update, list, remove)

Other:
  help      Show help for a command
  version   Show version

Global Flags:
  --server URL       Server URL (default: http://localhost:5005/git)
  --token TOKEN      Bearer token for auth
  --repo NAME        Repository name
  --namespace NS     Namespace
  --json             Output as JSON
  -h, --help         Show help
  -v, --version      Show version

Examples:
  pgh commit -a -m "Add feature"
  pgh push
  pgh pull --rebase
  pgh repo list
  pgh issue list --repo pear-git
  pgh issue create "Fix bug" -d "Description" --repo pear-git
  pgh search code "escapeHtml" --repo pear-git`)
}