OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-lab / lib / git-sync-manager.js

148 lines · javascript ref:
import { SSHManager } from './ssh-manager.js'
import chalk from 'chalk'

export class GitSyncManager {
  constructor(agentConfigs, options = {}) {
    this.agents = agentConfigs
    this.bareRepoPath = options.bareRepoPath || '/home/fleeky/git-repos/hypercore-kb.git'
    this.sshManagers = new Map()

    // Initialize SSH managers
    for (const config of agentConfigs) {
      this.sshManagers.set(config.name, new SSHManager(config))
    }
  }

  async connect() {
    console.log(chalk.blue('🔌 Connecting to agents...\n'))
    for (const manager of this.sshManagers.values()) {
      await manager.connect()
    }
  }

  async syncAll() {
    console.log(chalk.blue('\n📦 Starting git sync...\n'))

    // Step 1: Check tabertron has clean working directory
    await this.verifyCleanWorkingDir('tabertron')

    // Step 2: Push from tabertron to bare repo
    await this.pushToBareRepo('tabertron')

    // Step 3: Pull on all other agents
    for (const agent of this.agents) {
      if (agent.name !== 'tabertron') {
        await this.pullFromRemote(agent.name)
      }
    }

    // Step 4: Verify all agents are on same commit
    await this.verifySync()

    console.log(chalk.green('\n✅ Git sync complete!\n'))
  }

  async verifyCleanWorkingDir(agentName) {
    const manager = this.sshManagers.get(agentName)
    // Use parent directory (hypercore-kb) not subdirectory (hyper-fleeky)
    const workDir = manager.config.workDir.replace('/hyper-fleeky', '')

    console.log(chalk.cyan(`📋 Checking ${agentName} working directory...`))

    const result = await manager.exec(`cd ${workDir} && git status --porcelain`)

    if (result.stdout.trim() !== '') {
      console.log(chalk.yellow(`⚠️  ${agentName} has uncommitted changes:`))
      console.log(result.stdout)

      throw new Error(
        `${agentName} has uncommitted changes. Please commit or stash first.\n` +
        `Run: cd ${workDir} && git status`
      )
    }

    console.log(chalk.green(`✅ ${agentName} working directory is clean`))
  }

  async pushToBareRepo(agentName) {
    const manager = this.sshManagers.get(agentName)
    // Use parent directory (hypercore-kb) not subdirectory (hyper-fleeky)
    const workDir = manager.config.workDir.replace('/hyper-fleeky', '')

    console.log(chalk.cyan(`📤 Pushing from ${agentName} to bare repo...`))

    // Get current branch
    const branchResult = await manager.exec(`cd ${workDir} && git rev-parse --abbrev-ref HEAD`)
    const branch = branchResult.stdout.trim()

    // Push to bare repo
    const result = await manager.exec(
      `cd ${workDir} && git push ${this.bareRepoPath} ${branch}:master 2>&1`
    )

    if (result.exitCode !== 0 && !result.stdout.includes('Everything up-to-date')) {
      throw new Error(`Git push failed on ${agentName}:\n${result.stderr || result.stdout}`)
    }

    console.log(chalk.green(`✅ Pushed from ${agentName}`))
  }

  async pullFromRemote(agentName) {
    const manager = this.sshManagers.get(agentName)
    // Use parent directory (hypercore-kb) not subdirectory (hyper-fleeky)
    const workDir = manager.config.workDir.replace('/hyper-fleeky', '')

    console.log(chalk.cyan(`📥 Pulling on ${agentName}...`))

    // Pull from remote (origin)
    const result = await manager.exec(
      `cd ${workDir} && git fetch origin && git reset --hard origin/master 2>&1`
    )

    if (result.exitCode !== 0) {
      throw new Error(`Git pull failed on ${agentName}:\n${result.stderr || result.stdout}`)
    }

    console.log(chalk.green(`✅ Pulled on ${agentName}`))
  }

  async verifySync() {
    console.log(chalk.cyan('\n🔍 Verifying all agents on same commit...\n'))

    const commits = new Map()

    // Get commit hash from each agent
    for (const agent of this.agents) {
      const manager = this.sshManagers.get(agent.name)
      // Use parent directory (hypercore-kb) not subdirectory (hyper-fleeky)
      const workDir = manager.config.workDir.replace('/hyper-fleeky', '')

      const result = await manager.exec(`cd ${workDir} && git rev-parse HEAD`)
      const commit = result.stdout.trim()

      commits.set(agent.name, commit)
      console.log(`  ${agent.name}: ${commit.substring(0, 8)}`)
    }

    // Check all commits match
    const commitHashes = Array.from(commits.values())
    const allMatch = commitHashes.every(c => c === commitHashes[0])

    if (!allMatch) {
      console.log(chalk.red('\n❌ Agents are on different commits!'))
      for (const [name, commit] of commits) {
        console.log(`  ${name}: ${commit}`)
      }
      throw new Error('Git sync verification failed - agents on different commits')
    }

    console.log(chalk.green('\n✅ All agents on same commit\n'))
  }

  async disconnect() {
    for (const manager of this.sshManagers.values()) {
      await manager.disconnect()
    }
  }
}