#!/usr/bin/env pear
/**
* pear-git-remote - Pear app for git pear:// URL support
*
* Install:
* pear run pear://<key>/pear-git-remote install
*
* Then use:
* git clone pear://<server-key>/<repo>
*/
// Load Bare runtime compatibility
await import('bare-node-runtime/global')
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import process from 'bare-process'
import { PetnameManager } from './lib/petnames.js'
import { TopicConnector } from './lib/topic-manager.js'
import { GitP2PClient } from './lib/git-p2p-handler.js'
// Pear is available as a global when running under Pear runtime
const Pear = globalThis.Pear
const HOME = os.homedir()
const BIN_DIR = path.join(HOME, '.local', 'bin')
const HELPER_PATH = path.join(BIN_DIR, 'git-remote-pear')
// Get our pear key for the wrapper script
const pearKey = Pear?.config?.key?.toString('hex') || 'unknown'
// Use Pear.config.args when running under Pear, otherwise process.argv
const isPear = typeof Pear !== 'undefined'
const args = isPear ? (Pear.config.args || []) : process.argv.slice(2)
const command = args[0]
async function main() {
// Check if we're being called as git remote helper
// Git passes: git-remote-pear <remote-name> <url>
if (command && command.startsWith('pear://')) {
// Called by git with URL as first arg
await runGitHelper(command, command)
return
}
if (args.length >= 2 && args[1]?.startsWith('pear://')) {
// Called by git with remote name and URL
await runGitHelper(args[0], args[1])
return
}
// Otherwise handle as CLI command
switch (command) {
case 'install':
await install()
break
case 'uninstall':
await uninstall()
break
case 'status':
await status()
break
case 'helper':
// Explicit helper mode: pear run ... helper <remote> <url>
await runGitHelper(args[1], args[2] || args[1])
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-remote - Git remote helper for pear:// URLs
Usage:
pear run pear://${pearKey.slice(0, 8)}... install Install git helper
pear run pear://${pearKey.slice(0, 8)}... uninstall Remove git helper
pear run pear://${pearKey.slice(0, 8)}... status Check installation
After installing:
git clone pear://<server-key>/<repo>
Example:
git clone pear://3cf3266a57b3ef069b724a4330200d72b0ad81daea7311b7d2a2369b0469f199/wirecore
`)
}
async function install() {
console.log('Installing git-remote-pear...')
// Create bin directory
try {
await fs.promises.mkdir(BIN_DIR, { recursive: true })
} catch (err) {
if (err.code !== 'EEXIST') throw err
}
// Create wrapper script that calls pear run
const wrapper = `#!/bin/bash
# git-remote-pear - installed by pear-git-remote
# Calls the Pear app to handle pear:// git URLs
exec pear run pear://${pearKey} helper "$@"
`
await fs.promises.writeFile(HELPER_PATH, wrapper, { mode: 0o755 })
console.log(`✓ Installed ${HELPER_PATH}`)
// Check PATH
const pathEnv = process.env.PATH || ''
if (!pathEnv.includes('.local/bin')) {
console.log(`
Add ~/.local/bin to PATH:
echo 'export PATH="\$HOME/.local/bin:\$PATH"' >> ~/.bashrc
source ~/.bashrc
`)
} else {
console.log('✓ ~/.local/bin is in PATH')
}
console.log(`
Ready! Clone repos with:
git clone pear://<server-key>/<repo>
`)
}
async function uninstall() {
try {
await fs.promises.unlink(HELPER_PATH)
console.log(`✓ Removed ${HELPER_PATH}`)
} catch (err) {
if (err.code === 'ENOENT') {
console.log('git-remote-pear is not installed')
} else {
throw err
}
}
}
async function status() {
try {
await fs.promises.access(HELPER_PATH, fs.constants.X_OK)
console.log(`✓ Installed at ${HELPER_PATH}`)
// Read and show the pear key it points to
const content = await fs.promises.readFile(HELPER_PATH, 'utf-8')
const match = content.match(/pear:\/\/([a-f0-9]+)\//)
if (match) {
console.log(` Points to: pear://${match[1].slice(0, 16)}...`)
}
const pathEnv = process.env.PATH || ''
if (pathEnv.includes('.local/bin')) {
console.log('✓ ~/.local/bin is in PATH')
} else {
console.log('⚠ ~/.local/bin is NOT in PATH')
}
} catch {
console.log('✗ Not installed')
console.log(` Run: pear run pear://${pearKey.slice(0, 8)}... install`)
}
}
async function runGitHelper(remoteName, remoteUrl) {
// Initialize petname manager
const petnames = new PetnameManager()
await petnames.init()
// Parse pear:// URL
let parsed
try {
parsed = petnames.parseUrl(remoteUrl)
} catch (err) {
console.error(`error: ${err.message}`)
process.exit(128)
}
const { key, repo, isPetname, petname } = parsed
if (isPetname) {
console.error(`[pear] Resolved ${petname} -> ${key.slice(0, 8)}...`)
}
// Read commands from git
const stdin = process.stdin
const stdout = process.stdout
const stderr = process.stderr
let buffer = Buffer.alloc(0)
let commandMode = true
stdin.on('data', async (chunk) => {
if (!commandMode) return
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
buffer = Buffer.concat([buffer, chunkBuf])
let newlineIdx
while ((newlineIdx = buffer.indexOf(0x0a)) !== -1) {
const line = buffer.slice(0, newlineIdx).toString('utf-8').trim()
buffer = buffer.slice(newlineIdx + 1)
if (line === '') continue
const shouldPipe = await handleCommand(line, key, repo, stdout, stderr)
if (shouldPipe) {
commandMode = false
return
}
}
})
stdin.on('end', () => {
process.exit(0)
})
}
async function handleCommand(command, serverKey, repoName, stdout, stderr) {
const [cmd, ...cmdArgs] = command.split(' ')
switch (cmd) {
case 'capabilities':
stdout.write('connect\n')
stdout.write('\n')
return false
case 'connect':
const service = cmdArgs[0]
await handleConnect(service, serverKey, repoName, stdout, stderr)
return true
case 'list':
stderr.write('[pear] list not implemented, use connect\n')
stdout.write('\n')
return false
default:
stderr.write(`[pear] Unknown command: ${cmd}\n`)
process.exit(1)
return false
}
}
async function handleConnect(service, serverKey, repoName, stdout, stderr) {
stderr.write(`[pear] Connecting to ${serverKey.slice(0, 8)}.../${repoName}\n`)
const connector = new TopicConnector({ timeout: 30000 })
await connector.init()
try {
stderr.write('[pear] Searching for peers...\n')
const conn = await connector.connect(serverKey, repoName)
stderr.write('[pear] Connected!\n')
const client = new GitP2PClient()
await client.initiate(conn, service, repoName)
stdout.write('\n')
process.stdin.pipe(conn)
conn.pipe(stdout)
conn.on('close', () => {
stderr.write('[pear] Connection closed\n')
process.exit(0)
})
conn.on('error', (err) => {
stderr.write(`[pear] Error: ${err.message}\n`)
process.exit(128)
})
} catch (err) {
stderr.write(`[pear] Failed: ${err.message}\n`)
await connector.destroy()
process.exit(128)
}
}
main().catch(err => {
console.error(`Error: ${err.message}`)
process.exit(1)
})