OverviewFilesCommitsIssuesDocsReleasesPeers

autobase-doctor / bin / autobase-doctor.js

192 lines · javascript ref:
#!/usr/bin/env bare

/**
 * autobase-doctor — Diagnose and repair corrupted Autobase instances.
 *
 * Usage:
 *   autobase-doctor scan <storage> [--namespace <ns>]
 *   autobase-doctor repair <storage> [--namespace <ns>] [--dry-run]
 *   autobase-doctor inspect <storage> --namespace <ns>
 *   autobase-doctor check <storage> --namespace <ns>
 */

import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import path from 'bare-path'
import process from 'bare-process'
import { scanWriterCore, listOps } from '../lib/scanner.js'
import { truncateWriterCore, resetSystemState, hasPoisonedSystemState } from '../lib/repair.js'

const args = process.argv.slice(2)
const command = args[0]
const storagePath = args[1]

if (!command || !storagePath || command === '--help' || command === '-h') {
  console.log(`autobase-doctor — Diagnose and repair corrupted Autobase instances

Usage:
  autobase-doctor scan <storage>                    Scan all namespaces for poisoned ops
  autobase-doctor repair <storage> [--dry-run]      Fix all poisoned Autobases
  autobase-doctor inspect <storage> --namespace <ns> Dump all ops in a namespace
  autobase-doctor check <storage> --namespace <ns>   Check system state health

Options:
  --namespace <ns>   Target a specific corestore namespace (e.g. "linked:myrepo")
  --registry <name>  Registry core name for namespace discovery (default: "registry")
  --registry-prefix  Registry key prefix (default: "config:issueAutobase:")
  --dry-run          Show what would be done without making changes

Examples:
  autobase-doctor scan ./pear-git-storage
  autobase-doctor repair ./pear-git-storage --dry-run
  autobase-doctor inspect ./pear-git-storage --namespace linked:pear-git
  autobase-doctor check ./pear-git-storage --namespace linked:myrepo`)
  process.exit(0)
}

let namespace = null
let registryName = 'registry'
let registryPrefix = 'config:issueAutobase:'
let dryRun = false

for (let i = 2; i < args.length; i++) {
  if (args[i] === '--namespace' && args[i + 1]) { namespace = args[i + 1]; i++ }
  else if (args[i] === '--registry' && args[i + 1]) { registryName = args[i + 1]; i++ }
  else if (args[i] === '--registry-prefix' && args[i + 1]) { registryPrefix = args[i + 1]; i++ }
  else if (args[i] === '--dry-run') { dryRun = true }
}

const storageDir = path.resolve(storagePath)
const store = new Corestore(storageDir)
await store.ready()

/**
 * Discover namespaces from registry or use explicit --namespace
 */
async function getNamespaces () {
  if (namespace) return [namespace]

  const core = store.get({ name: registryName })
  const db = new Hyperbee(core, { keyEncoding: 'utf-8', valueEncoding: 'json' })
  await db.ready()

  const namespaces = []
  for await (const entry of db.createReadStream({
    gt: registryPrefix, lt: registryPrefix + '\xff'
  })) {
    const name = entry.key.slice(registryPrefix.length)
    namespaces.push('linked:' + name)
  }

  await db.close()
  return namespaces
}

// ── Commands ──────────────────────────────────────────────

if (command === 'scan') {
  const namespaces = await getNamespaces()
  console.log('Scanning ' + namespaces.length + ' namespaces in ' + storageDir + '\n')

  let poisoned = 0
  for (const ns of namespaces) {
    const result = await scanWriterCore(store.namespace(ns))
    if (result) {
      poisoned++
      const label = ns.replace('linked:', '')
      console.log(label + ': POISONED at op ' + result.poisonIndex + '/' + result.totalOps)
      console.log('  ' + result.op.type + ' serverKey=' + (result.op.serverKey || '').slice(0, 24) + '...')
    }
  }

  console.log('\n' + poisoned + '/' + namespaces.length + ' poisoned')

} else if (command === 'repair') {
  const namespaces = await getNamespaces()
  console.log('Repairing ' + namespaces.length + ' namespaces in ' + storageDir)
  console.log('Dry run: ' + dryRun + '\n')

  let fixed = 0
  for (const nsName of namespaces) {
    const ns = store.namespace(nsName)
    const label = nsName.replace('linked:', '')

    // Step 1: Check writer core for poison
    const scan = await scanWriterCore(ns)
    if (scan) {
      console.log(label + ': poison at op ' + scan.poisonIndex + '/' + scan.totalOps)
      if (!dryRun) {
        const { before, after } = await truncateWriterCore(ns, scan.poisonIndex)
        console.log('  ✓ writer truncated ' + before + ' → ' + after)
      } else {
        console.log('  [DRY RUN] would truncate writer ' + scan.totalOps + ' → ' + scan.poisonIndex)
      }
    }

    // Step 2: Check system state (even if writer was already clean)
    const sysInfo = await hasPoisonedSystemState(ns)
    if (sysInfo.poisoned) {
      console.log((scan ? '' : label + ': ') + 'system poisoned (writable=' + sysInfo.writable + ' members=' + sysInfo.members + ')')
      if (!dryRun) {
        const result = await resetSystemState(ns)
        if (result.systemTruncated) console.log('  ✓ system batch reset (' + result.systemTruncated + ' blocks)')
        if (result.viewTruncated) console.log('  ✓ view batch reset (' + result.viewTruncated + ' blocks)')
        if (result.bootCleared) console.log('  ✓ cleared autobase/boot')
      } else {
        console.log('  [DRY RUN] would reset system batch (' + sysInfo.systemBatchLength + ' blocks)')
      }
    }

    if (scan || sysInfo.poisoned) fixed++
  }

  console.log('\nFixed: ' + fixed + '/' + namespaces.length)
  if (dryRun) console.log('(Dry run — re-run without --dry-run to apply)')
  else if (fixed > 0) console.log('Restart Autobase consumers to rebuild clean state.')

} else if (command === 'inspect') {
  if (!namespace) {
    console.error('--namespace required for inspect')
    process.exit(1)
  }

  const ns = store.namespace(namespace)
  const ops = await listOps(ns)
  console.log(namespace + ': ' + ops.length + ' ops\n')

  for (const { index, op, size, error } of ops) {
    if (error) {
      console.log('[' + index + '] ERROR: ' + error)
    } else if (op) {
      const summary = op.type + (op.title ? ': ' + op.title.slice(0, 60) : '') +
        (op.serverKey ? ' key=' + op.serverKey.slice(0, 24) + '...' : '')
      console.log('[' + index + '] ' + summary)
    } else {
      console.log('[' + index + '] (undecoded, ' + size + ' bytes)')
    }
  }

} else if (command === 'check') {
  if (!namespace) {
    console.error('--namespace required for check')
    process.exit(1)
  }

  const ns = store.namespace(namespace)
  const info = await hasPoisonedSystemState(ns)
  console.log(namespace + ':')
  console.log('  writable: ' + info.writable)
  console.log('  members: ' + info.members)
  console.log('  indexers: ' + info.indexers)
  console.log('  local length: ' + info.localLength)
  console.log('  system batch: ' + info.systemBatchLength)
  console.log('  poisoned: ' + info.poisoned)

} else {
  console.error('Unknown command: ' + command)
  console.error('Run autobase-doctor --help for usage')
  process.exit(1)
}

await store.close()