OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / bin / pear-git-admin

301 lines · text ref:
#!/usr/bin/env node
/**
 * Pear Git Admin CLI
 *
 * CLI tool to interact with a running pear-git server.
 *
 * Usage:
 *   pear-git-admin [command] [options]
 *
 * Commands:
 *   users                  List all users
 *   user <userId>          Get user details
 *   user-update <userId>   Update user (--name, --permissions)
 *   identities             List all identities
 *   add-identity <userId>  Add identity to user (--pubkey)
 *   challenges             List active challenges
 *   reset-rate-limit       Reset rate limits (--ip or all)
 *   stats                  Show server statistics
 *   fix-identities         Fix user identities arrays
 *   restart                Restart server (graceful shutdown)
 *   shutdown               Shutdown server (no restart)
 *
 * Options:
 *   --server URL           Server URL (default: http://localhost:5005/git)
 *   --token TOKEN          Bearer token for auth
 *   --pubkey KEY           Public key for auth
 *   --keypair KEY          Full 132-char keypair for signed auth
 */

const args = process.argv.slice(2)

// Parse options
const options = {
  server: 'http://localhost:5005/git',
  token: null,
  pubkey: null,
  keypair: null
}

const positional = []

for (let i = 0; i < args.length; i++) {
  const arg = args[i]
  if (arg === '--server' && args[i + 1]) {
    options.server = args[++i]
  } else if (arg === '--token' && args[i + 1]) {
    options.token = args[++i]
  } else if (arg === '--pubkey' && args[i + 1]) {
    options.pubkey = args[++i]
  } else if (arg === '--keypair' && args[i + 1]) {
    options.keypair = args[++i]
  } else if (arg === '--name' && args[i + 1]) {
    options.name = args[++i]
  } else if (arg === '--permissions' && args[i + 1]) {
    options.permissions = args[++i].split(',')
  } else if (arg === '--ip' && args[i + 1]) {
    options.ip = args[++i]
  } else if (arg === '--userId' && args[i + 1]) {
    options.userId = args[++i]
  } else if (!arg.startsWith('--')) {
    positional.push(arg)
  }
}

const command = positional[0]
const commandArg = positional[1]

// Help
if (!command || command === 'help' || command === '--help') {
  console.log(`
Pear Git Admin CLI

Usage:
  pear-git-admin <command> [options]

Commands:
  users                  List all users
  user <userId>          Get user details
  user-update <userId>   Update user (--name, --permissions)
  identities             List all identities
  add-identity <userId>  Add identity to user (--pubkey)
  challenges             List active challenges
  reset-rate-limit       Reset rate limits (--ip or all)
  stats                  Show server statistics
  fix-identities         Fix user identities arrays (--userId or all)
  restart                Restart server (graceful shutdown, exit 0)
  shutdown               Shutdown server (no restart)

Options:
  --server URL           Server URL (default: http://localhost:5005/git)
  --token TOKEN          Bearer token for auth
  --keypair KEY          Full 132-char keypair for signed auth

Examples:
  pear-git-admin users --keypair "your132charkeypair"
  pear-git-admin user user:abc123 --keypair "..."
  pear-git-admin user-update user:abc123 --name "New Name" --keypair "..."
  pear-git-admin stats --keypair "..."
  pear-git-admin fix-identities --keypair "..."
`)
  process.exit(0)
}

// Build auth headers
async function getAuthHeaders() {
  const headers = {
    'Content-Type': 'application/json'
  }

  if (options.token) {
    headers['Authorization'] = `Bearer ${options.token}`
  } else if (options.keypair) {
    // Get challenge first
    const challengeRes = await fetch(`${options.server}/api/auth/challenge`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ context: 'cli-admin' })
    })
    const challenge = await challengeRes.json()

    // Sign challenge
    const publicKey = options.keypair.substring(0, 44)
    const secretKey = options.keypair.substring(44)

    // Dynamic import for signing
    const { sign } = await import('./sign-helper.mjs')
    const signature = await sign(challenge.challenge, secretKey)

    headers['X-Wirecore-PublicKey'] = publicKey
    headers['X-Wirecore-Signature'] = signature
    headers['X-Wirecore-Challenge'] = challenge.challengeId
  } else if (options.pubkey) {
    headers['X-Wirecore-PublicKey'] = options.pubkey
  }

  return headers
}

// Make API request
async function apiRequest(method, path, body = null) {
  const headers = await getAuthHeaders()

  const fetchOptions = {
    method,
    headers
  }

  if (body) {
    fetchOptions.body = JSON.stringify(body)
  }

  const res = await fetch(`${options.server}${path}`, fetchOptions)
  const data = await res.json()

  if (!res.ok) {
    console.error('Error:', data.error || res.statusText)
    process.exit(1)
  }

  return data
}

// Commands
async function main() {
  try {
    switch (command) {
      case 'users': {
        const data = await apiRequest('GET', '/api/admin/users')
        // Handle both formats: {users: [...]} or plain array
        const users = Array.isArray(data) ? data : (data.users || [])
        const count = Array.isArray(data) ? data.length : (data.count || users.length)
        console.log(`\nUsers (${count}):\n`)
        for (const user of users) {
          console.log(`  ${user.userId}`)
          console.log(`    Name: ${user.name || '(none)'}`)
          console.log(`    Permissions: ${user.globalPermissions?.join(', ') || 'none'}`)
          console.log(`    Identities: ${user.identities?.length || 0}`)
          console.log()
        }
        break
      }

      case 'user': {
        if (!commandArg) {
          console.error('Usage: user <userId>')
          process.exit(1)
        }
        const data = await apiRequest('GET', `/api/admin/users/${encodeURIComponent(commandArg)}`)
        console.log('\nUser:', JSON.stringify(data.user, null, 2))
        console.log('\nIdentities:', JSON.stringify(data.identities, null, 2))
        break
      }

      case 'user-update': {
        if (!commandArg) {
          console.error('Usage: user-update <userId> --name "Name" --permissions "admin,read,write"')
          process.exit(1)
        }
        const body = {}
        if (options.name) body.name = options.name
        if (options.permissions) body.globalPermissions = options.permissions

        const data = await apiRequest('PATCH', `/api/admin/users/${encodeURIComponent(commandArg)}`, body)
        console.log('Updated:', JSON.stringify(data.user, null, 2))
        break
      }

      case 'identities': {
        const data = await apiRequest('GET', '/api/admin/identities')
        // Handle both formats
        const identities = Array.isArray(data) ? data : (data.identities || [])
        const count = Array.isArray(data) ? data.length : (data.count || identities.length)
        console.log(`\nIdentities (${count}):\n`)
        for (const id of identities) {
          console.log(`  ${id.identityId || id.key}`)
          console.log(`    Method: ${id.method}`)
          console.log(`    User: ${id.userId}`)
          console.log(`    PublicKey: ${id.publicKey?.substring(0, 20) || id.credentials?.publicKey?.substring(0, 20)}...`)
          console.log()
        }
        break
      }

      case 'add-identity': {
        if (!commandArg || !options.pubkey) {
          console.error('Usage: add-identity <userId> --pubkey <publicKey>')
          process.exit(1)
        }
        const data = await apiRequest('POST', `/api/admin/users/${encodeURIComponent(commandArg)}/identities`, {
          publicKey: options.pubkey
        })
        console.log('Added identity:', JSON.stringify(data, null, 2))
        break
      }

      case 'challenges': {
        const data = await apiRequest('GET', '/api/admin/challenges')
        console.log('\nChallenge Stats:', JSON.stringify(data.stats, null, 2))
        console.log('\nActive Challenges:')
        for (const c of data.challenges) {
          console.log(`  ${c.id} - used: ${c.used}, age: ${Math.round(c.ageMs / 1000)}s`)
        }
        break
      }

      case 'reset-rate-limit': {
        const body = options.ip ? { ip: options.ip } : {}
        const data = await apiRequest('POST', '/api/admin/rate-limit/reset', body)
        console.log(data.message)
        break
      }

      case 'stats': {
        const data = await apiRequest('GET', '/api/admin/stats')
        console.log('\nServer Statistics:\n')
        console.log('Server:')
        console.log(`  Uptime: ${Math.round(data.server.uptime)}s`)
        console.log(`  Memory: ${Math.round(data.server.memory.heapUsed / 1024 / 1024)}MB`)
        console.log(`  Repos: ${data.server.repos}`)
        console.log('\nRegistry:')
        console.log(`  Users: ${data.registry.users}`)
        console.log(`  Identities: ${data.registry.identities}`)
        console.log('\nChallenges:')
        console.log(`  Total: ${data.challenges.total}`)
        console.log(`  Active: ${data.challenges.active}`)
        console.log(`  Used: ${data.challenges.used}`)
        break
      }

      case 'fix-identities': {
        const body = options.userId ? { userId: options.userId } : {}
        const data = await apiRequest('POST', '/api/admin/fix/user-identities', body)
        console.log(`Fixed ${data.usersFixed} users, added ${data.identitiesAdded} identities`)
        break
      }

      case 'restart': {
        const data = await apiRequest('POST', '/api/admin/restart', {})
        console.log(data.message)
        break
      }

      case 'shutdown': {
        const data = await apiRequest('POST', '/api/admin/shutdown', {})
        console.log(data.message)
        break
      }

      default:
        console.error(`Unknown command: ${command}`)
        console.error('Run with --help for usage')
        process.exit(1)
    }
  } catch (err) {
    console.error('Error:', err.message)
    process.exit(1)
  }
}

main()