#!/usr/bin/env node
import { spawn } from 'bare-subprocess'
import dgram from 'bare-dgram'
import os from 'bare-os'
/**
* LAN Discovery for pear-git
*
* Features:
* - Announce presence on local network
* - Discover other pear-git instances
* - mDNS/Bonjour service advertisement
* - UDP broadcast discovery (fallback)
*/
export class LANDiscovery {
constructor(options = {}) {
this.port = options.port || 5005
this.serviceName = options.serviceName || 'pear-git'
this.identity = options.identity
this.repos = options.repos || []
this.peers = new Map() // IP → peer info
this.discoveryPort = 45454 // UDP broadcast port
}
/**
* Start announcing presence on LAN
*/
async start() {
// Try mDNS first (requires avahi-daemon on Linux)
await this.startMDNS()
// Fallback to UDP broadcast
await this.startUDPBroadcast()
// Listen for other pear-git instances
try {
await this.listenForPeers()
} catch (err) {
console.log('LAN peer listener unavailable:', err.message)
}
}
/**
* Announce via mDNS/Bonjour
*/
async startMDNS() {
try {
const hostname = os.hostname()
const serviceName = `${this.serviceName}@${hostname}`
const identityHex = 'pending'
// Use avahi-publish-service on Linux
const publish = spawn('avahi-publish-service', [
serviceName,
'_pear-git._tcp',
this.port.toString(),
`identity=${identityHex}`,
`repos=${this.repos.length}`,
`version=1.0.0`
])
publish.on('error', (err) => {
console.error('mDNS announcement failed:', err.message)
console.log('Install avahi-daemon: sudo apt install avahi-daemon avahi-utils')
})
publish.on('exit', (code) => {
if (code !== 0) {
console.error('mDNS service exited with code', code)
}
})
console.log('✓ Announcing via mDNS:')
console.log(` Service: ${serviceName}`)
console.log(` Port: ${this.port}`)
console.log(` Identity: ${identityHex.slice(0, 16)}...`)
this.mdnsProcess = publish
} catch (err) {
console.error('Failed to start mDNS:', err.message)
}
}
/**
* Announce via UDP broadcast (fallback)
* DISABLED: bare-dgram doesn't support setBroadcast()
*/
async startUDPBroadcast() {
console.log('⚠ UDP broadcast disabled (bare-dgram limitation)')
console.log(' Using mDNS only for LAN discovery')
// TODO: Implement UDP broadcast when bare-dgram adds broadcast support
}
/**
* Listen for announcements from other peers
*/
async listenForPeers() {
const socket = dgram.createSocket('udp4')
socket.on('message', (msg, rinfo) => {
try {
const data = JSON.parse(msg.toString())
if (data.type === 'pear-git-announcement') {
// Don't add ourselves
const publicKey = this.identity.keetIdentity?.identityKeyPair?.publicKey
const ourIdentity = publicKey ? publicKey.toString('hex') : null
if (data.identity === ourIdentity) {
return
}
// Add or update peer
this.peers.set(rinfo.address, {
ip: rinfo.address,
port: data.port,
hostname: data.hostname,
identity: data.identity,
repos: data.repos,
lastSeen: Date.now()
})
console.log(`📡 Discovered peer: ${data.hostname} (${rinfo.address}:${data.port})`)
}
} catch (err) {
// Invalid message, ignore
}
})
socket.bind(this.discoveryPort, () => {
console.log(`✓ Listening for peers on UDP ${this.discoveryPort}`)
})
this.listenerSocket = socket
// Clean up stale peers every minute
this.cleanupInterval = setInterval(() => {
const now = Date.now()
for (const [ip, peer] of this.peers.entries()) {
if (now - peer.lastSeen > 120000) { // 2 minutes
console.log(`Peer timed out: ${peer.hostname} (${ip})`)
this.peers.delete(ip)
}
}
}, 60000)
}
/**
* Discover pear-git instances using mDNS
*/
async discoverViaMDNS() {
return new Promise((resolve) => {
const peers = []
const browse = spawn('avahi-browse', ['-ptr', '_pear-git._tcp', '--resolve'])
browse.stdout.on('data', (data) => {
const text = data.toString()
// Parse avahi-browse output
const lines = text.split('\n')
for (const line of lines) {
if (line.includes('hostname') && line.includes('address')) {
// Extract hostname and IP
const hostnameMatch = line.match(/hostname \[([^\]]+)\]/)
const addressMatch = line.match(/address \[([^\]]+)\]/)
const portMatch = line.match(/port \[([^\]]+)\]/)
if (hostnameMatch && addressMatch && portMatch) {
peers.push({
hostname: hostnameMatch[1],
ip: addressMatch[1],
port: parseInt(portMatch[1])
})
}
}
}
})
browse.on('exit', () => {
resolve(peers)
})
// Timeout after 5 seconds
setTimeout(() => {
browse.kill()
resolve(peers)
}, 5000)
})
}
/**
* Get all discovered peers
*/
getPeers() {
return Array.from(this.peers.values())
}
/**
* Stop discovery
*/
async stop() {
if (this.mdnsProcess) {
this.mdnsProcess.kill()
}
if (this.broadcastInterval) {
clearInterval(this.broadcastInterval)
}
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval)
}
if (this.broadcastSocket) {
this.broadcastSocket.close()
}
if (this.listenerSocket) {
this.listenerSocket.close()
}
}
/**
* Get local IP addresses
* Note: bare-os doesn't have networkInterfaces, return placeholder
*/
getLocalIPs() {
// bare-os doesn't support networkInterfaces()
// Use hostname as the best available network address
var hostname = 'localhost'
try { hostname = os.hostname() } catch (e) {}
return [{
interface: 'hostname',
address: hostname
}, {
interface: 'local',
address: 'localhost'
}]
}
/**
* Render LAN discovery UI
*/
renderDiscoveryUI() {
const localIPs = this.getLocalIPs()
const peers = this.getPeers()
return `
<div style="background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 20px; margin-top: 20px;">
<h3 style="margin-bottom: 15px;">🌐 Local Network</h3>
<div style="margin-bottom: 20px;">
<h4 style="color: #8b949e; margin-bottom: 10px;">Your Addresses:</h4>
${localIPs.map(ip => `
<div style="background: #0d1117; padding: 10px; border-radius: 4px; margin-bottom: 5px;">
<code>${ip.interface}: ${ip.address}:${this.port}</code>
</div>
`).join('')}
</div>
<div>
<h4 style="color: #8b949e; margin-bottom: 10px;">Discovered Peers (${peers.length}):</h4>
${peers.length === 0
? '<div style="color: #6e7681;">No peers discovered yet. Other pear-git instances will appear here automatically.</div>'
: peers.map(peer => `
<div style="background: #0d1117; padding: 10px; border-radius: 4px; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center;">
<div>
<div><strong>${peer.hostname}</strong></div>
<div style="font-size: 12px; color: #8b949e;">
${peer.ip}:${peer.port} • ${peer.repos} repos
</div>
</div>
<button onclick="location.href='http://${peer.ip}:${peer.port}'" style="background: #238636; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
Connect
</button>
</div>
`).join('')
}
</div>
<div style="margin-top: 20px; padding: 15px; background: #0d1117; border-radius: 4px; border-left: 3px solid #58a6ff;">
<div style="font-weight: 600; margin-bottom: 5px;">💡 Clone via LAN:</div>
<code style="background: #161b22; padding: 5px; border-radius: 3px;">
git clone http://${localIPs[0]?.address || 'localhost'}:${this.port}/username/repo-name
</code>
</div>
</div>
`;
}
}
// CLI Test - Disabled (must use pear-git-server.js as entry point)
// This module is designed to be imported, not run directly