| … | … | @@ -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 | |
| 10 | import { PghClient } from '../lib/client.js' |
| 11 | import fs from 'bare-fs' |
| 12 | import path from 'bare-path' |
| 13 | import os from 'bare-os' |
| 14 | |
| 15 | const argv = typeof Bare !== 'undefined' ? Bare.argv : process.argv |
| 16 | const args = argv.slice(2) |
| 17 | const cmd = args[0] |
| 18 | const sub = args[1] |
| 19 | |
| 20 | // Load config |
| 21 | const configDir = path.join(os.homedir(), '.config', 'pgh') |
| 22 | const configPath = path.join(configDir, 'config.json') |
| 23 | let config = { server: 'http://localhost:5005' } |
| 24 | try { config = { ...config, ...JSON.parse(fs.readFileSync(configPath, 'utf-8')) } } catch {} |
| 25 | |
| 26 | // Server override from flag |
| 27 | const serverFlag = args.indexOf('--server') |
| 28 | if (serverFlag >= 0) config.server = args[serverFlag + 1] |
| 29 | |
| 30 | const client = new PghClient(config) |
| 31 | |
| 32 | async 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 | |
| 121 | Commands: |
| 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 | |
| 137 | Options: |
| 138 | --server <url> Override server URL |
| 139 | `) |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | function log (data) { console.log(JSON.stringify(data, null, 2)) } |
| 144 | function logList (arr, fmt) { arr.forEach(item => console.log(fmt(item))) } |
| 145 | function usage (msg) { console.log('Usage:', msg) } |
| 146 | function 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 | |
| 153 | run().catch(err => console.error('Error:', err.message)) |