#!/usr/bin/env bare
/**
* pgh — CLI for pear-git
*
* Talks to pear-git's JSON API over HTTP.
* Config: ~/.config/pgh/config.json { server, token }
*/
import { PghClient } from '../lib/client.js'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
const argv = typeof Bare !== 'undefined' ? Bare.argv : process.argv
const args = argv.slice(2)
const cmd = args[0]
const sub = args[1]
// Load config
const configDir = path.join(os.homedir(), '.config', 'pgh')
const configPath = path.join(configDir, 'config.json')
let config = { server: 'http://localhost:5005' }
try { config = { ...config, ...JSON.parse(fs.readFileSync(configPath, 'utf-8')) } } catch {}
// Server override from flag
const serverFlag = args.indexOf('--server')
if (serverFlag >= 0) config.server = args[serverFlag + 1]
// Token from env takes priority
const envToken = typeof Bare !== 'undefined' ? Bare.env?.PGH_TOKEN : process.env.PGH_TOKEN
if (envToken) config.token = envToken
const client = new PghClient(config)
async function run () {
switch (cmd) {
case 'status':
return log(await client.status())
case 'repos':
return logList(await client.repos(), r => `${r.name} ${r.lastCommit ? ago(r.lastCommit) : ''}`)
case 'repo':
if (!sub) return usage('pgh repo <name>')
return log(await client.repo(sub))
case 'issues': {
if (!sub) return usage('pgh issues <repo>')
const issues = await client.issues(sub)
return logList(issues, i => `#${i.id} [${i.status}] ${i.title}`)
}
case 'issue': {
if (sub === 'create') {
const repo = args[2]; const title = args[3]
if (!repo || !title) return usage('pgh issue create <repo> <title> [body]')
return log(await client.createIssue(repo, { title, body: args[4] || '', author: 'pgh' }))
}
if (sub === 'close') {
const repo = args[2]; const id = args[3]
if (!repo || !id) return usage('pgh issue close <repo> <id>')
return log(await client.updateIssue(repo, id, { status: 'closed' }))
}
if (sub === 'reopen') {
const repo = args[2]; const id = args[3]
if (!repo || !id) return usage('pgh issue reopen <repo> <id>')
return log(await client.updateIssue(repo, id, { status: 'open' }))
}
if (sub === 'comment') {
const repo = args[2]; const id = args[3]; const text = args[4]
if (!repo || !id || !text) return usage('pgh issue comment <repo> <id> <text>')
return log(await client.addComment(repo, id, { body: text, author: 'pgh' }))
}
if (!sub) return usage('pgh issue <repo> <id>')
const id = args[2]
if (!id) return usage('pgh issue <repo> <id>')
return log(await client.issue(sub, id))
}
case 'search': {
if (!sub) return usage('pgh search <query>')
const result = await client.search(sub)
console.log(`${result.total} results for "${result.query}"`)
for (const r of result.results) {
console.log(` ${r.repo}/${r.path}:${r.line} ${r.content}`)
}
return
}
case 'deploy': {
if (sub === 'status') return log(await client.deployStatus())
if (sub === 'config') {
const repo = args[2]
if (!repo) return usage('pgh deploy config <repo>')
return log(await client.deployConfig(repo))
}
if (sub === 'log') {
const repo = args[2]
if (!repo) return usage('pgh deploy log <repo>')
const entries = await client.deployLog(repo)
return logList(entries, e => `[${e.status}] ${e.commitHash?.slice(0, 8) || ''} ${e.timestamp ? ago(e.timestamp) : ''}`)
}
if (!sub) return usage('pgh deploy <repo>')
console.log(`Deploying ${sub}...`)
return log(await client.deploy(sub))
}
case 'orgs':
return logList(await client.orgs(), o => `${o.name} ${o.repos?.length || 0} repos`)
case 'users':
return logList(await client.users(), u => `${u.username || u.userId?.slice(0, 12)} ${u.name || ''}`)
case 'auth': {
const { challengeId, challenge } = await client.challenge()
console.log('Challenge:', challenge)
console.log('Sign this challenge with your key and call:')
console.log(` pgh verify <signature>`)
// TODO: auto-sign with local keypair
return
}
case 'config':
console.log('Server:', config.server)
console.log('Config:', configPath)
if (config.token) console.log('Token:', config.token.slice(0, 16) + '...')
else console.log('Token: not set')
return
case 'config-set': {
if (!sub) return usage('pgh config-set server <url>')
const val = args[2]
if (!val) return usage('pgh config-set <key> <value>')
config[sub] = val
try { fs.mkdirSync(configDir, { recursive: true }) } catch {}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
console.log(`Set ${sub} = ${val}`)
return
}
case 'app': {
if (sub === 'install') {
const repo = args[2]
const nameFlag = args.indexOf('--name')
const name = nameFlag >= 0 ? args[nameFlag + 1] : (args[3] || repo)
if (!repo) return usage('pgh app install <repo> [--name name]')
const installDir = path.join(os.homedir(), '.local', 'share', name)
const binDir = path.join(os.homedir(), '.local', 'bin')
const binPath = path.join(binDir, name)
console.log(`Installing ${repo} → ${installDir}`)
// Determine clone URL (punch:// key or server-based)
const cloneUrl = repo.startsWith('punch://') ? repo : `${config.server}/${repo}.git`
const { spawn: spawnChild } = await import('bare-subprocess')
try {
await execCmd(spawnChild, ['git', 'clone', '--depth', '1', '-b', 'release', cloneUrl, installDir])
} catch (err) {
// If dir exists, do a pull instead
if (err.message.includes('already exists')) {
console.log('Directory exists, pulling latest...')
await execCmd(spawnChild, ['git', '-C', installDir, 'fetch', 'origin', 'release'])
await execCmd(spawnChild, ['git', '-C', installDir, 'reset', '--hard', 'FETCH_HEAD'])
} else {
throw err
}
}
// Create launcher script
try { fs.mkdirSync(binDir, { recursive: true }) } catch {}
fs.writeFileSync(binPath, `#!/bin/sh\nexec bare "${installDir}/index.js" "$@"\n`)
fs.chmodSync(binPath, 0o755)
console.log(`Installed. Run: ${name}`)
return
}
if (sub === 'list') {
const shareDir = path.join(os.homedir(), '.local', 'share')
const binDir = path.join(os.homedir(), '.local', 'bin')
try {
const bins = fs.readdirSync(binDir)
for (const b of bins) {
try {
const content = fs.readFileSync(path.join(binDir, b), 'utf-8')
if (content.includes('.local/share/') && content.includes('bare')) {
console.log(b)
}
} catch {}
}
} catch { console.log('No apps installed') }
return
}
if (sub === 'remove') {
const name = args[2]
if (!name) return usage('pgh app remove <name>')
const installDir = path.join(os.homedir(), '.local', 'share', name)
const binPath = path.join(os.homedir(), '.local', 'bin', name)
try { fs.rmSync(binPath) } catch {}
try { fs.rmSync(installDir, { recursive: true }) } catch {}
console.log(`Removed ${name}`)
return
}
return usage('pgh app install|list|remove')
}
default:
console.log(`pgh — CLI for pear-git
Commands:
pgh status Server status
pgh repos List repositories
pgh repo <name> Repo details
pgh issues <repo> List issues
pgh issue <repo> <id> Issue details
pgh issue create <repo> <title> [body] Create issue
pgh issue close <repo> <id> Close issue
pgh issue reopen <repo> <id> Reopen issue
pgh issue comment <repo> <id> <text> Add comment
pgh search <query> Search code across repos
pgh deploy <repo> Trigger deploy
pgh deploy status Deploy status
pgh deploy config <repo> Deploy config
pgh deploy log <repo> Deploy history
pgh orgs List organizations
pgh users List users
pgh auth Start auth flow
pgh config Show config
pgh config-set <key> <value> Set config
pgh app install <repo> [--name n] Install app from release branch
pgh app list List installed apps
pgh app remove <name> Uninstall app
Options:
--server <url> Override server URL
Environment:
PGH_TOKEN Auth token (overrides config)
`)
}
}
function execCmd (spawnChild, [cmd, ...cmdArgs]) {
return new Promise((resolve, reject) => {
const child = spawnChild(cmd, cmdArgs, { stdio: ['ignore', 'pipe', 'pipe'] })
const out = []; const err = []
child.stdout.on('data', d => out.push(d))
child.stderr.on('data', d => err.push(d))
child.on('exit', code => code === 0 ? resolve(Buffer.concat(out).toString()) : reject(new Error(Buffer.concat(err).toString() || `exit ${code}`)))
child.on('error', reject)
})
}
function log (data) { console.log(JSON.stringify(data, null, 2)) }
function logList (arr, fmt) { arr.forEach(item => console.log(fmt(item))) }
function usage (msg) { console.log('Usage:', msg) }
function ago (ts) {
const d = Date.now() - new Date(ts).getTime()
if (d < 3600000) return Math.floor(d / 60000) + 'm ago'
if (d < 86400000) return Math.floor(d / 3600000) + 'h ago'
return Math.floor(d / 86400000) + 'd ago'
}
run().catch(err => console.error('Error:', err.message))