#!/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()