OverviewFilesCommitsIssuesDocsReleasesPeers

pgh-legacy commits / a9969cc

Initial pgh: standalone CLI for pear-git JSON API
bin/pgh.js — CLI entry point (bare or node) lib/client.js — PghClient HTTP wrapper Commands: status, repos, repo, issues, issue, issue create, deploy, deploy status, orgs, users, auth, config, config-set, app install Config: ~/.config/pgh/config.json { server, token } Works with any pear-git instance (v1 or v2) that serves JSON API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
a9969cc5bb39205301958cec4918302d1a85e6c1
by plankatron <fleeky@gmail.com> · 2026-03-30T11:59:37.000Z

Changed files (4) +216 -0

.gitignore +1 -0
……@@ -0,0 +1 @@
1node_modules/
bin/pgh.js +153 -0
……@@ -0,0 +1,153 @@
1#!/usr/bin/env bare
2
3/**
4 * pgh — CLI for pear-git
5 *
6 * Talks to pear-git's JSON API over HTTP.
7 * Config: ~/.config/pgh/config.json { server, token }
8 */
9
10import { PghClient } from '../lib/client.js'
11import fs from 'bare-fs'
12import path from 'bare-path'
13import os from 'bare-os'
14
15const argv = typeof Bare !== 'undefined' ? Bare.argv : process.argv
16const args = argv.slice(2)
17const cmd = args[0]
18const sub = args[1]
19
20// Load config
21const configDir = path.join(os.homedir(), '.config', 'pgh')
22const configPath = path.join(configDir, 'config.json')
23let config = { server: 'http://localhost:5005' }
24try { config = { ...config, ...JSON.parse(fs.readFileSync(configPath, 'utf-8')) } } catch {}
25
26// Server override from flag
27const serverFlag = args.indexOf('--server')
28if (serverFlag >= 0) config.server = args[serverFlag + 1]
29
30const client = new PghClient(config)
31
32async function run () {
33 switch (cmd) {
34 case 'status':
35 return log(await client.status())
36
37 case 'repos':
38 return logList(await client.repos(), r => `${r.name} ${r.lastCommit ? ago(r.lastCommit) : ''}`)
39
40 case 'repo':
41 if (!sub) return usage('pgh repo <name>')
42 return log(await client.repo(sub))
43
44 case 'issues': {
45 if (!sub) return usage('pgh issues <repo>')
46 const issues = await client.issues(sub)
47 return logList(issues, i => `#${i.id} [${i.status}] ${i.title}`)
48 }
49
50 case 'issue': {
51 if (sub === 'create') {
52 const repo = args[2]
53 const title = args[3]
54 if (!repo || !title) return usage('pgh issue create <repo> <title> [body]')
55 const body = args[4] || ''
56 return log(await client.createIssue(repo, { title, body, author: 'pgh' }))
57 }
58 if (!sub) return usage('pgh issue <repo> <id> OR pgh issue create <repo> <title>')
59 const id = args[2]
60 if (!id) return usage('pgh issue <repo> <id>')
61 return log(await client.issue(sub, id))
62 }
63
64 case 'deploy': {
65 if (sub === 'status') return log(await client.deployStatus())
66 if (!sub) return usage('pgh deploy <repo>')
67 console.log(`Deploying ${sub}...`)
68 return log(await client.deploy(sub))
69 }
70
71 case 'orgs':
72 return logList(await client.orgs(), o => `${o.name} ${o.repos?.length || 0} repos`)
73
74 case 'users':
75 return logList(await client.users(), u => `${u.username || u.userId?.slice(0, 12)} ${u.name || ''}`)
76
77 case 'auth': {
78 const { challengeId, challenge } = await client.challenge()
79 console.log('Challenge:', challenge)
80 console.log('Sign this challenge with your key and call:')
81 console.log(` pgh verify <signature>`)
82 // TODO: auto-sign with local keypair
83 return
84 }
85
86 case 'config':
87 console.log('Server:', config.server)
88 console.log('Config:', configPath)
89 if (config.token) console.log('Token:', config.token.slice(0, 16) + '...')
90 else console.log('Token: not set')
91 return
92
93 case 'config-set': {
94 if (!sub) return usage('pgh config-set server <url>')
95 const val = args[2]
96 if (!val) return usage('pgh config-set <key> <value>')
97 config[sub] = val
98 try { fs.mkdirSync(configDir, { recursive: true }) } catch {}
99 fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
100 console.log(`Set ${sub} = ${val}`)
101 return
102 }
103
104 case 'app': {
105 if (sub === 'install') {
106 const repo = args[2]
107 const name = args[3] || repo
108 if (!repo) return usage('pgh app install <repo> [name]')
109 const installDir = path.join(os.homedir(), '.local', 'share', name)
110 console.log(`Cloning ${repo} release branch to ${installDir}...`)
111 // TODO: implement git clone of release branch
112 console.log('Not yet implemented — use: git clone --depth 1 -b release <url>')
113 return
114 }
115 return usage('pgh app install <repo> [name]')
116 }
117
118 default:
119 console.log(`pgh — CLI for pear-git
120
121Commands:
122 pgh status Server status
123 pgh repos List repositories
124 pgh repo <name> Repo details
125 pgh issues <repo> List issues
126 pgh issue <repo> <id> Issue details
127 pgh issue create <repo> <title> [body] Create issue
128 pgh deploy <repo> Trigger deploy
129 pgh deploy status Deploy status
130 pgh orgs List organizations
131 pgh users List users
132 pgh auth Start auth flow
133 pgh config Show config
134 pgh config-set <key> <value> Set config
135 pgh app install <repo> [name] Install app
136
137Options:
138 --server <url> Override server URL
139`)
140 }
141}
142
143function log (data) { console.log(JSON.stringify(data, null, 2)) }
144function logList (arr, fmt) { arr.forEach(item => console.log(fmt(item))) }
145function usage (msg) { console.log('Usage:', msg) }
146function ago (ts) {
147 const d = Date.now() - new Date(ts).getTime()
148 if (d < 3600000) return Math.floor(d / 60000) + 'm ago'
149 if (d < 86400000) return Math.floor(d / 3600000) + 'h ago'
150 return Math.floor(d / 86400000) + 'd ago'
151}
152
153run().catch(err => console.error('Error:', err.message))
lib/client.js +52 -0
……@@ -0,0 +1,52 @@
1/**
2 * PGH API Client — HTTP client for pear-git JSON API
3 *
4 * Used by CLI and programmatically.
5 */
6
7export class PghClient {
8 constructor (opts = {}) {
9 this.server = opts.server || 'http://localhost:5005'
10 this.token = opts.token || null
11 }
12
13 async get (path) {
14 const res = await fetch(this.server + path, {
15 headers: this._headers()
16 })
17 if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
18 return res.json()
19 }
20
21 async post (path, body) {
22 const res = await fetch(this.server + path, {
23 method: 'POST',
24 headers: { ...this._headers(), 'Content-Type': 'application/json' },
25 body: JSON.stringify(body)
26 })
27 if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
28 return res.json()
29 }
30
31 _headers () {
32 const h = { Accept: 'application/json' }
33 if (this.token) h.Authorization = `Bearer ${this.token}`
34 return h
35 }
36
37 // Convenience methods
38 status () { return this.get('/api/status') }
39 repos () { return this.get('/api/repos') }
40 repo (name) { return this.get(`/api/repos/${name}`) }
41 issues (repo) { return this.get(`/api/repos/${repo}/issues`) }
42 issue (repo, id) { return this.get(`/api/repos/${repo}/issues/${id}`) }
43 createIssue (repo, data) { return this.post(`/api/repos/${repo}/issues`, data) }
44 comments (repo, id) { return this.get(`/api/repos/${repo}/issues/${id}/comments`) }
45 addComment (repo, id, data) { return this.post(`/api/repos/${repo}/issues/${id}/comments`, data) }
46 orgs () { return this.get('/api/orgs') }
47 users () { return this.get('/api/users') }
48 deploy (repo) { return this.post(`/api/deploy/${repo}`, {}) }
49 deployStatus () { return this.get('/api/deploy/status') }
50 challenge () { return this.get('/api/auth/challenge') }
51 verify (data) { return this.post('/api/auth/verify', data) }
52}
package.json +10 -0
……@@ -0,0 +1,10 @@
1{
2 "name": "pgh",
3 "version": "1.0.0",
4 "description": "CLI for pear-git — manage repos, issues, deploys, and identity via JSON API",
5 "type": "module",
6 "bin": {
7 "pgh": "./bin/pgh.js"
8 },
9 "main": "lib/client.js"
10}