#!/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()