#!/usr/bin/env bare
/**
* pear-git-client - Simple P2P git client
*
* A lightweight client for cloning git repos over P2P without running a full server.
*
* Usage:
* pear-git-client servers - List known servers
* pear-git-client add <name> <key> - Add a server
* pear-git-client remove <name> - Remove a server
* pear-git-client browse <server> - Browse repos on a server (TODO)
* pear-git-client clone <url> [dir] - Clone a repo
* pear-git-client discover - Find servers on LAN
* pear-git-client install - Install git-remote-pear helper
*/
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import process from 'bare-process'
import { spawn } from 'bare-subprocess'
const HOME = os.homedir()
const CONFIG_DIR = path.join(HOME, '.pear-git')
const SERVERS_FILE = path.join(CONFIG_DIR, 'servers.json')
const BIN_DIR = path.join(HOME, '.local', 'bin')
const [,, command, ...args] = process.argv
async function main() {
await ensureConfigDir()
switch (command) {
case 'servers':
case 'list':
await listServers()
break
case 'add':
await addServer(args[0], args[1])
break
case 'remove':
case 'rm':
await removeServer(args[0])
break
case 'clone':
await cloneRepo(args[0], args[1])
break
case 'discover':
await discoverLAN()
break
case 'install':
await installHelper()
break
case 'help':
case '--help':
case '-h':
case undefined:
showHelp()
break
default:
console.error(`Unknown command: ${command}`)
showHelp()
process.exit(1)
}
}
function showHelp() {
console.log(`
pear-git-client - Simple P2P Git Client
Commands:
servers List known pear-git servers
add <name> <key> Add a server (creates petname)
remove <name> Remove a server
clone <url> [dir] Clone a repo via pear://
discover Find pear-git servers on LAN
install Install git-remote-pear helper
help Show this help
Examples:
# Add a server
pear-git-client add tabertron 3cf3266a57b3ef069b724a4330200d72b0ad81daea7311b7d2a2369b0469f199
# Clone using petname
pear-git-client clone pear://tabertron/wirecore
# Clone using full key
pear-git-client clone pear://3cf3266a.../wirecore ./my-wirecore
# Find servers on local network
pear-git-client discover
`)
}
async function ensureConfigDir() {
try {
await fs.promises.mkdir(CONFIG_DIR, { recursive: true })
} catch (err) {
if (err.code !== 'EEXIST') throw err
}
}
async function loadServers() {
try {
const data = await fs.promises.readFile(SERVERS_FILE, 'utf-8')
return JSON.parse(data)
} catch (err) {
if (err.code === 'ENOENT') return {}
throw err
}
}
async function saveServers(servers) {
await fs.promises.writeFile(SERVERS_FILE, JSON.stringify(servers, null, 2))
}
async function listServers() {
const servers = await loadServers()
const entries = Object.entries(servers)
if (entries.length === 0) {
console.log('No servers configured.')
console.log('')
console.log('Add one with:')
console.log(' pear-git-client add <name> <64-char-hex-key>')
return
}
console.log('Known pear-git servers:')
console.log('')
for (const [name, data] of entries) {
console.log(` ${name}`)
console.log(` Key: ${data.key.slice(0, 16)}...`)
if (data.description) {
console.log(` ${data.description}`)
}
console.log('')
}
console.log(`Clone with: git clone pear://<name>/<repo>`)
}
async function addServer(name, key) {
if (!name || !key) {
console.error('Usage: pear-git-client add <name> <key>')
console.error('')
console.error('Example:')
console.error(' pear-git-client add tabertron 3cf3266a57b3ef069b724a4330200d72b0ad81daea7311b7d2a2369b0469f199')
process.exit(1)
}
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
console.error('Invalid name. Use lowercase letters, numbers, and hyphens.')
process.exit(1)
}
if (!/^[a-f0-9]{64}$/i.test(key)) {
console.error('Invalid key. Must be 64 character hex string.')
process.exit(1)
}
const servers = await loadServers()
servers[name] = {
key: key.toLowerCase(),
addedAt: Date.now()
}
await saveServers(servers)
// Also save to petnames for git-remote-pear compatibility
const petnamesFile = path.join(CONFIG_DIR, 'remotes.json')
let petnames = {}
try {
petnames = JSON.parse(await fs.promises.readFile(petnamesFile, 'utf-8'))
} catch {}
petnames[name] = { key: key.toLowerCase(), addedAt: Date.now() }
await fs.promises.writeFile(petnamesFile, JSON.stringify(petnames, null, 2))
console.log(`✓ Added server "${name}"`)
console.log('')
console.log(`Clone repos with:`)
console.log(` git clone pear://${name}/<repo>`)
}
async function removeServer(name) {
if (!name) {
console.error('Usage: pear-git-client remove <name>')
process.exit(1)
}
const servers = await loadServers()
if (!servers[name]) {
console.error(`Server "${name}" not found`)
process.exit(1)
}
delete servers[name]
await saveServers(servers)
console.log(`✓ Removed server "${name}"`)
}
async function cloneRepo(url, targetDir) {
if (!url) {
console.error('Usage: pear-git-client clone <pear://...> [directory]')
process.exit(1)
}
if (!url.startsWith('pear://')) {
console.error('URL must start with pear://')
process.exit(1)
}
// Resolve petname if needed
const servers = await loadServers()
let resolvedUrl = url
const match = url.match(/^pear:\/\/([^\/]+)\/(.+)$/)
if (match) {
const [, keyOrName, repo] = match
if (!/^[a-f0-9]{64}$/i.test(keyOrName) && servers[keyOrName]) {
resolvedUrl = `pear://${servers[keyOrName].key}/${repo}`
console.log(`Resolved ${keyOrName} -> ${servers[keyOrName].key.slice(0, 8)}...`)
}
}
// Build git clone command
const gitArgs = ['clone', resolvedUrl]
if (targetDir) {
gitArgs.push(targetDir)
}
console.log(`Cloning ${url}...`)
console.log('')
// Spawn git with our helper in PATH
const scriptDir = path.dirname(new URL(import.meta.url).pathname)
const helperDir = path.join(scriptDir, '..', 'pear-git-remote', 'bin')
const gitProcess = spawn('git', gitArgs, {
stdio: 'inherit',
env: {
...process.env,
PATH: `${helperDir}:${BIN_DIR}:${process.env.PATH}`
}
})
gitProcess.on('exit', (code) => {
process.exit(code || 0)
})
}
async function discoverLAN() {
console.log('Discovering pear-git servers on LAN...')
console.log('')
// Use mDNS to find _pear-git._tcp services
// This is a simplified version - real implementation would use multicast-dns
const Hyperswarm = (await import('hyperswarm')).default
const swarm = new Hyperswarm()
// TODO: Implement proper mDNS discovery
// For now, just show a message
console.log('LAN discovery requires mDNS support.')
console.log('')
console.log('Servers announce themselves as:')
console.log(' _pear-git._tcp on port 5005')
console.log('')
console.log('Use: avahi-browse -r _pear-git._tcp')
console.log('Or: dns-sd -B _pear-git._tcp')
await swarm.destroy()
}
async function installHelper() {
console.log('Installing git-remote-pear helper...')
await fs.promises.mkdir(BIN_DIR, { recursive: true })
const helperPath = path.join(BIN_DIR, 'git-remote-pear')
const scriptDir = path.dirname(new URL(import.meta.url).pathname)
const actualHelper = path.join(scriptDir, '..', 'git-remote-pear', 'bin', 'git-remote-pear')
// Create symlink or wrapper
const wrapper = `#!/usr/bin/env bare
// git-remote-pear - installed by pear-git-client
import('${actualHelper}')
`
await fs.promises.writeFile(helperPath, wrapper, { mode: 0o755 })
console.log(`✓ Installed to ${helperPath}`)
const pathEnv = process.env.PATH || ''
if (!pathEnv.includes(BIN_DIR)) {
console.log('')
console.log('Add to PATH:')
console.log(` echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc`)
console.log(' source ~/.bashrc')
}
console.log('')
console.log('Now use: git clone pear://<key>/<repo>')
}
main().catch(err => {
console.error('Error:', err.message)
process.exit(1)
})