#!/usr/bin/env node
import { openDB, listApps, getCorestoreStats, formatBytes } from '../lib/rocksdb-inspector.js'
import { findCorestores, getDirSize, getLastModified, isLocked, getLockHolder } from '../lib/scanner.js'
import { openDrive, getDriveStats, discoverDrives } from '../lib/drive-inspector.js'
import { enumerateCores } from '../lib/core-enumerator.js'
import { analyzeCoreDiskUsage, formatAnalysis } from '../lib/deep-analyzer.js'
import { walkHyperbeeBlocks, decodeNode, decodeDriveEntry, stripSubEncoder, decodeHeader } from '../lib/block-reader.js'
import { listDriveFromDB, buildTree, formatTree, formatFlat } from '../lib/drive-ls.js'
import { extractDrive } from '../lib/drive-extract.js'
import { corestoreDU, formatDU } from '../lib/du.js'
import b4a from 'b4a'
import { existsSync } from 'fs'
import { join, resolve } from 'path'
import { homedir } from 'os'
const args = process.argv.slice(2)
const command = args[0]
const HELP = `
hyper-inspector - Inspect, audit, and manage Hypercore corestores
Commands:
scan [dir] Scan a directory for all corestores (default: ~/)
audit [dir] Full audit with sizes, lock status, and staleness
du [dir] Full Hyperdrive disk usage across all corestores
inspect <db-path> Inspect a corestore's RocksDB (path to db/ dir)
apps <db-path> List pear:// apps in a platform corestore
cores <storage-path> Enumerate all cores in a corestore (path with CORESTORE file)
drives <storage-path> Discover all Hyperdrives in a corestore
drive <storage-path> Open and traverse a Hyperdrive, list all files
drive <storage-path> --key <hex> Open a specific drive by key
ls <db-path> <dataPtr> List all files in a Hyperdrive (by RocksDB dataPointer)
usage <db-path> Deep analysis: which cores consume how many bytes on disk
blocks <storage-path> --key <hex> Read raw Hyperbee blocks, decode file entries
extract <storage-path> --data-ptr <n> --out <dir> Copy files OUT of a Hyperdrive (read-only)
Options:
--readonly Open databases in read-only mode (for locked DBs)
--limit <n> Max keys to scan (default: 100000)
--key <hex> Specify a Hypercore key
--data-ptr <n> Metadata core dataPointer (from "cores"/"usage") for extract
--out <dir> Destination directory for extract
--filter <substr> Only extract paths containing this string
--max-bytes <n> Skip files larger than this (e.g. 5M)
--dry-run List what would be extracted without writing
--help Show this help
Examples:
hyper-inspector scan
hyper-inspector audit
hyper-inspector cores ~/.config/hypernote/server-data
hyper-inspector drive ~/.config/hypernote/server-data
hyper-inspector ls ~/.config/hypernote/server-data/db 3 --readonly
hyper-inspector extract ~/.config/hypernote/server-data --data-ptr 3 --out /tmp/dump --readonly
hyper-inspector inspect ~/.config/pear/corestores/platform/db --readonly
hyper-inspector apps ~/.config/pear/corestores/platform/db --readonly
`
if (!command || command === '--help' || command === 'help') {
console.log(HELP)
process.exit(0)
}
if (command === 'scan') {
const dir = args[1] || homedir()
console.log(`\nScanning ${dir} for Hypercore storage...\n`)
const stores = findCorestores(dir)
let totalSize = 0
for (const store of stores) {
totalSize += store.size
const modified = getLastModified(store.path)
const modStr = modified ? modified.toISOString().slice(0, 10) : 'unknown'
console.log(` ${formatBytes(store.size).padStart(10)} ${modStr} ${store.path}`)
}
console.log(`\n Total: ${formatBytes(totalSize)} across ${stores.length} locations\n`)
}
if (command === 'inspect') {
const dbPath = resolve(args[1] || '')
const readOnly = args.includes('--readonly')
const limit = parseInt(args[args.indexOf('--limit') + 1]) || 100000
if (!dbPath || !existsSync(dbPath)) {
console.error('Error: provide a valid path to a db/ directory')
process.exit(1)
}
const locked = isLocked(dbPath)
if (locked && !readOnly) {
const holder = getLockHolder(dbPath)
console.error(`Database is locked by ${holder?.command} (PID ${holder?.pid})`)
console.error('Use --readonly to open in read-only mode')
process.exit(1)
}
console.log(`\nInspecting: ${dbPath}`)
console.log(`Mode: ${readOnly ? 'read-only' : 'read-write'}`)
const db = await openDB(dbPath, { readOnly })
const stats = await getCorestoreStats(db)
console.log(`\nCorestore column family:`)
console.log(` Keys scanned: ${stats.totalKeys}`)
console.log(` Value bytes: ${formatBytes(stats.totalBytes)}`)
console.log(`\nKey type breakdown:`)
for (const t of stats.typeBreakdown) {
console.log(` ${t.type.padEnd(16)} ${t.count.toString().padStart(8)} keys ${t.mb.padStart(10)} MB`)
}
if (stats.readable.length > 0) {
console.log(`\nSample readable values (${stats.readable.length}):`)
for (const r of stats.readable.slice(0, 30)) {
const label = KEY_TYPES_LABEL[r.type] || `0x${r.type.toString(16)}`
try {
const obj = JSON.parse(r.value)
if (obj.slug || obj.name || obj.title || obj.id) {
console.log(` [${label}] ${obj.title || obj.name || obj.slug || obj.id}`)
continue
}
} catch {}
console.log(` [${label}] ${r.value.slice(0, 100)}`)
}
}
const parentPath = dbPath.replace(/\/db\/?$/, '')
const diskSize = getDirSize(parentPath)
console.log(`\nDisk usage: ${formatBytes(diskSize)}`)
await db.close()
}
if (command === 'apps') {
const dbPath = resolve(args[1] || '')
const readOnly = args.includes('--readonly')
if (!dbPath || !existsSync(dbPath)) {
console.error('Error: provide a valid path to a platform db/ directory')
process.exit(1)
}
const db = await openDB(dbPath, { readOnly })
const { pearApps, fileSources } = await listApps(db)
const appRegistrations = pearApps.filter(a => a.prefix === 0x03)
const appVersions = pearApps.filter(a => a.prefix === 0x06)
console.log(`\n=== Pear Apps (${appRegistrations.length} registered) ===\n`)
for (const app of appRegistrations) {
const versionEntry = appVersions.find(v => v.url === app.url)
const hasVersion = versionEntry ? 'active' : 'no-version'
const dkeyMatch = app.storage.match(/by-dkey\/([a-f0-9]+)/)
const randomMatch = app.storage.match(/by-random\/([a-f0-9]+)/)
const storageId = dkeyMatch ? `dkey:${dkeyMatch[1].slice(0, 12)}...` : randomMatch ? `random:${randomMatch[1].slice(0, 12)}...` : ''
const storagePath = app.storage.match(/\/home\/[^\s|]+/)?.[0]
let sizeStr = ''
if (storagePath && existsSync(storagePath)) {
sizeStr = formatBytes(getDirSize(storagePath))
}
console.log(` ${app.url}`)
console.log(` status: ${hasVersion} storage: ${storageId} size: ${sizeStr || 'n/a'}`)
}
console.log(`\n=== Staging Sources (${fileSources.length}) ===\n`)
for (const f of fileSources) {
console.log(` ${f.path}`)
}
await db.close()
}
if (command === 'cores') {
const storagePath = resolve(args[1] || '')
const readOnly = args.includes('--readonly')
if (!storagePath || !existsSync(storagePath)) {
console.error('Error: provide a valid corestore path (directory with CORESTORE file)')
process.exit(1)
}
const dbPath = join(storagePath, 'db')
const locked = isLocked(dbPath)
if (locked && !readOnly) {
const holder = getLockHolder(dbPath)
console.error(`Database is locked by ${holder?.command} (PID ${holder?.pid})`)
console.error('Use --readonly to open in read-only mode')
process.exit(1)
}
console.log(`\nEnumerating cores in: ${storagePath}`)
console.log(`Mode: ${readOnly ? 'read-only' : 'read-write'}\n`)
const cores = await enumerateCores(dbPath, { readOnly })
console.log(`Found ${cores.length} cores:\n`)
for (const core of cores) {
const alias = core.alias
? `${core.alias.name} (ns: ${core.alias.namespace.slice(0, 8)}...)`
: '(unnamed)'
const key = core.publicKey ? `key: ${core.publicKey.slice(0, 16)}...` : ''
const len = core.length != null ? `len: ${core.length}` : ''
console.log(` #${String(core.corePointer).padEnd(4)} ${alias.padEnd(30)} ${len.padEnd(12)} ${key}`)
console.log(` dkey: ${core.discoveryKey.slice(0, 32)}...`)
}
}
if (command === 'drives') {
const storagePath = resolve(args[1] || '')
const readOnly = args.includes('--readonly')
if (!storagePath || !existsSync(storagePath)) {
console.error('Error: provide a valid corestore path')
process.exit(1)
}
console.log(`\nDiscovering Hyperdrives in: ${storagePath}\n`)
const drives = await discoverDrives(storagePath, { readOnly })
if (drives.length === 0) {
console.log('No Hyperdrives found (data may be Autobase/Hyperbee, not Hyperdrive)')
} else {
for (const d of drives) {
const alias = d.alias ? d.alias.name : '(unnamed)'
const rw = d.writable ? 'writable' : 'read-only'
console.log(` Hyperdrive: ${alias}`)
console.log(` key: ${d.key}`)
console.log(` content: ${d.contentKey || 'n/a'}`)
console.log(` version: ${d.version} bytes: ${formatBytes(d.byteLength)} ${rw}`)
if (d.filePaths.length > 0) {
console.log(` files (${d.filePaths.length} found):`)
for (const f of d.filePaths.slice(0, 15)) {
console.log(` ${f}`)
}
if (d.filePaths.length > 15) {
console.log(` ... and ${d.filePaths.length - 15} more`)
}
}
console.log()
}
}
}
if (command === 'drive') {
const storagePath = resolve(args[1] || '')
const keyIdx = args.indexOf('--key')
const key = keyIdx >= 0 ? args[keyIdx + 1] : null
if (!storagePath || !existsSync(storagePath)) {
console.error('Error: provide a valid corestore path')
process.exit(1)
}
const dbPath = join(storagePath, 'db')
const locked = isLocked(dbPath)
if (locked) {
const holder = getLockHolder(dbPath)
console.error(`Corestore is locked by ${holder?.command} (PID ${holder?.pid})`)
console.error('Cannot open a Hyperdrive while another process holds the lock.')
console.error('Stop the process first, or use "cores" / "inspect" with --readonly.')
process.exit(1)
}
console.log(`\nOpening Hyperdrive from: ${storagePath}`)
if (key) console.log(`Key: ${key}`)
const { store, drive } = await openDrive(storagePath, { key })
console.log(`\nDrive key: ${drive.key.toString('hex')}`)
console.log(`Version: ${drive.version}`)
console.log(`Writable: ${drive.writable}`)
const stats = await getDriveStats(drive)
console.log(`\nFiles: ${stats.fileCount}`)
console.log(`Total content: ${formatBytes(stats.totalBytes)}`)
console.log(`Blob blocks: ${stats.blobBlocks}`)
if (stats.topDirs.length > 0) {
console.log(`\n--- By directory ---`)
for (const d of stats.topDirs.slice(0, 15)) {
console.log(` ${formatBytes(d.bytes).padStart(10)} ${d.count.toString().padStart(5)} files ${d.dir}`)
}
}
if (stats.extensions.length > 0) {
console.log(`\n--- By extension ---`)
for (const e of stats.extensions.slice(0, 15)) {
console.log(` ${formatBytes(e.bytes).padStart(10)} ${e.count.toString().padStart(5)} files .${e.ext}`)
}
}
// Show first 50 files
console.log(`\n--- Files (first 50 of ${stats.files.length}) ---`)
for (const f of stats.files.slice(0, 50)) {
const size = f.byteLength ? formatBytes(f.byteLength) : '0 B'
const link = f.linkname ? ` -> ${f.linkname}` : ''
const exec = f.executable ? ' *' : ''
console.log(` ${size.padStart(10)} ${f.path}${exec}${link}`)
}
if (stats.files.length > 50) {
console.log(` ... and ${stats.files.length - 50} more files`)
}
await drive.close()
await store.close()
}
if (command === 'ls') {
const dbPath = resolve(args[1] || '')
const dataPtr = parseInt(args[2])
const readOnly = args.includes('--readonly')
const flat = args.includes('--flat')
const filterIdx = args.indexOf('--filter')
const filter = filterIdx >= 0 ? args[filterIdx + 1] : null
const depthIdx = args.indexOf('--depth')
const maxDepth = depthIdx >= 0 ? parseInt(args[depthIdx + 1]) : 3
const minIdx = args.indexOf('--min')
const minBytes = minIdx >= 0 ? parseSize(args[minIdx + 1]) : 0
if (!dbPath || isNaN(dataPtr)) {
console.error('Usage: hyper-inspector ls <db-path> <dataPointer> [--readonly] [--flat] [--filter <path>] [--depth <n>] [--min <size>]')
console.error(' dataPointer: the data pointer number from "cores" output')
console.error(' --flat: flat file list sorted by size')
console.error(' --filter: only show paths containing this string')
console.error(' --depth: max tree depth (default: 3)')
console.error(' --min: minimum file size to show (e.g., 1M, 100K)')
process.exit(1)
}
console.log(`\nListing Hyperdrive at dataPointer ${dataPtr} in ${dbPath}\n`)
const { header, files, blocksRead } = await listDriveFromDB(dbPath, dataPtr, { readOnly })
if (header) {
console.log(`Hyperbee: protocol="${header.protocol}"`)
if (header.contentFeed) {
console.log(`Content core: ${b4a.toString(header.contentFeed, 'hex')}`)
}
}
console.log(`Scanned ${blocksRead} blocks, found ${files.length} files`)
let totalBytes = 0
for (const f of files) totalBytes += f.bytes
console.log(`Total content: ${formatBytes(totalBytes)}\n`)
if (files.length === 0) {
console.log('No Hyperdrive file entries found.')
} else if (flat) {
console.log(formatFlat(files, { filter, limit: 200 }))
} else {
const tree = buildTree(files)
console.log(formatTree(tree, { maxDepth, minBytes, showFiles: maxDepth <= 2 }))
}
}
function parseSize (str) {
if (!str) return 0
const num = parseFloat(str)
if (str.endsWith('G') || str.endsWith('g')) return num * 1024 * 1024 * 1024
if (str.endsWith('M') || str.endsWith('m')) return num * 1024 * 1024
if (str.endsWith('K') || str.endsWith('k')) return num * 1024
return num
}
if (command === 'usage') {
const dbPath = resolve(args[1] || '')
const readOnly = args.includes('--readonly')
const sampleIdx = args.indexOf('--sample')
const sampleSize = sampleIdx >= 0 ? parseInt(args[sampleIdx + 1]) : 500000
if (!dbPath || !existsSync(dbPath)) {
console.error('Error: provide a valid path to a db/ directory')
process.exit(1)
}
console.log(`\nDeep disk usage analysis: ${dbPath}`)
console.log(`Sampling up to ${sampleSize} entries...\n`)
const analysis = await analyzeCoreDiskUsage(dbPath, { readOnly, sampleSize })
console.log(formatAnalysis(analysis))
}
if (command === 'blocks') {
const storagePath = resolve(args[1] || '')
const keyIdx = args.indexOf('--key')
const keyHex = keyIdx >= 0 ? args[keyIdx + 1] : null
const maxIdx = args.indexOf('--max')
const maxBlocks = maxIdx >= 0 ? parseInt(args[maxIdx + 1]) : 500
if (!storagePath || !keyHex) {
console.error('Usage: hyper-inspector blocks <storage-path> --key <hex> [--max <n>]')
process.exit(1)
}
const dbPath = join(storagePath, 'db')
const locked = isLocked(dbPath)
if (locked) {
const holder = getLockHolder(dbPath)
console.error(`Locked by ${holder?.command} (PID ${holder?.pid}). Cannot open core.`)
process.exit(1)
}
const Corestore = (await import('corestore')).default
const store = new Corestore(storagePath)
await store.ready()
const core = store.get({ key: Buffer.from(keyHex, 'hex') })
await core.ready()
console.log(`\nCore: len=${core.length} bytes=${formatBytes(core.byteLength)} writable=${core.writable}`)
const { header, entries } = await walkHyperbeeBlocks(core, { maxBlocks })
if (header) {
console.log(`\nHyperbee header: protocol="${header.protocol}"`)
if (header.contentFeed) {
console.log(`Content feed: ${b4a.toString(header.contentFeed, 'hex')}`)
}
}
console.log(`\nDecoded ${entries.length} Hyperbee entries from ${Math.min(core.length, maxBlocks)} blocks\n`)
// Try to decode as Hyperdrive file entries
let driveEntries = 0
let totalBlobBytes = 0
const filesByExt = new Map()
const filesByDir = new Map()
const allFiles = []
for (const entry of entries) {
const rawKey = stripSubEncoder(entry.key)
let filePath = ''
try {
filePath = b4a.toString(rawKey, 'utf8')
} catch {
continue
}
// Skip non-path entries
if (!filePath.startsWith('/')) continue
let driveEntry = null
try {
driveEntry = decodeDriveEntry(entry.value)
} catch {}
if (driveEntry) {
driveEntries++
const blob = driveEntry.blob
const bytes = blob?.byteLength || 0
totalBlobBytes += bytes
// By extension
const ext = filePath.includes('.') ? filePath.split('.').pop() : '(none)'
const extInfo = filesByExt.get(ext) || { count: 0, bytes: 0 }
extInfo.count++
extInfo.bytes += bytes
filesByExt.set(ext, extInfo)
// By top-level directory
const parts = filePath.split('/')
const topDir = parts.length > 2 ? '/' + parts[1] : '/'
const dirInfo = filesByDir.get(topDir) || { count: 0, bytes: 0 }
dirInfo.count++
dirInfo.bytes += bytes
filesByDir.set(topDir, dirInfo)
allFiles.push({
path: filePath,
blob,
executable: driveEntry.executable,
linkname: driveEntry.linkname,
bytes
})
}
}
if (driveEntries > 0) {
console.log(`=== Hyperdrive File Analysis ===`)
console.log(`Files: ${driveEntries}`)
console.log(`Total blob content: ${formatBytes(totalBlobBytes)}\n`)
// By directory
const sortedDirs = [...filesByDir.entries()].sort((a, b) => b[1].bytes - a[1].bytes)
console.log('--- By directory ---')
for (const [dir, info] of sortedDirs.slice(0, 20)) {
console.log(` ${formatBytes(info.bytes).padStart(10)} ${info.count.toString().padStart(5)} files ${dir}`)
}
// By extension
const sortedExts = [...filesByExt.entries()].sort((a, b) => b[1].bytes - a[1].bytes)
console.log('\n--- By extension ---')
for (const [ext, info] of sortedExts.slice(0, 20)) {
console.log(` ${formatBytes(info.bytes).padStart(10)} ${info.count.toString().padStart(5)} files .${ext}`)
}
// Largest files
allFiles.sort((a, b) => b.bytes - a.bytes)
console.log('\n--- Largest files ---')
for (const f of allFiles.slice(0, 20)) {
const blockInfo = f.blob ? `blocks[${f.blob.blockOffset}..${f.blob.blockOffset + f.blob.blockLength}]` : ''
console.log(` ${formatBytes(f.bytes).padStart(10)} ${f.path} ${blockInfo}`)
}
// Show blob block range summary
if (allFiles.length > 0 && allFiles[0].blob) {
let minBlock = Infinity
let maxBlock = 0
for (const f of allFiles) {
if (f.blob) {
minBlock = Math.min(minBlock, f.blob.blockOffset)
maxBlock = Math.max(maxBlock, f.blob.blockOffset + f.blob.blockLength)
}
}
console.log(`\nBlob block range: ${minBlock} - ${maxBlock} (${maxBlock - minBlock} blocks in content core)`)
}
} else {
// Not a Hyperdrive - show raw entries
console.log('Not a Hyperdrive (no file path entries found).')
console.log('Raw Hyperbee entries:')
for (const entry of entries.slice(0, 30)) {
let keyStr = ''
try {
keyStr = b4a.toString(entry.key, 'utf8')
if (!/^[\x20-\x7e\n\r\t]+$/.test(keyStr)) {
keyStr = `[hex: ${b4a.toString(entry.key, 'hex').slice(0, 40)}]`
}
} catch {
keyStr = `[hex: ${b4a.toString(entry.key, 'hex').slice(0, 40)}]`
}
const valLen = entry.value ? entry.value.length : 0
console.log(` seq=${entry.seq} ${keyStr} (val: ${valLen}B)`)
}
}
await core.close()
await store.close()
}
if (command === 'du') {
const dir = args[1] || homedir()
const depthIdx = args.indexOf('--depth')
const dirDepth = depthIdx >= 0 ? parseInt(args[depthIdx + 1]) : 2
const minIdx = args.indexOf('--min')
const minSize = minIdx >= 0 ? parseSize(args[minIdx + 1]) : 1024 * 1024
console.log(`\n=== Hyperdrive Disk Usage: ${dir} ===\n`)
const stores = findCorestores(dir)
for (const store of stores) {
const dbPath = join(store.path, 'db')
if (!existsSync(dbPath)) {
// Try nested corestore/db
const nested = join(store.path, 'corestore', 'db')
if (!existsSync(nested)) continue
}
const actualDbPath = existsSync(join(store.path, 'db'))
? join(store.path, 'db')
: join(store.path, 'corestore', 'db')
const diskSize = getDirSize(store.path)
const locked = isLocked(actualDbPath)
const lockInfo = locked ? getLockHolder(actualDbPath) : null
console.log(`--- ${store.path} (${formatBytes(diskSize)} on disk)${locked ? ` [LOCKED by ${lockInfo?.command}]` : ''} ---\n`)
try {
const result = await corestoreDU(actualDbPath, { readOnly: true, dirDepth })
console.log(formatDU(result, { dirDepth, minSize }))
} catch (err) {
if (err.message.includes('Column families not opened')) {
console.log(' (not a corestore RocksDB — legacy or different format)')
} else {
console.log(` Error: ${err.message.slice(0, 100)}`)
}
}
console.log('')
}
}
if (command === 'audit') {
const dir = args[1] || homedir()
console.log(`\n=== Hypercore Storage Audit: ${dir} ===\n`)
const stores = findCorestores(dir)
let totalSize = 0
const categories = { active: [], stale: [], junk: [], unknown: [] }
for (const store of stores) {
totalSize += store.size
const modified = getLastModified(store.path)
const now = new Date()
const ageMs = modified ? now - modified : Infinity
const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24))
const dbPath = join(store.path, 'db')
const parentDb = existsSync(dbPath) ? dbPath : join(store.path, 'corestore', 'db')
const locked = existsSync(parentDb) ? isLocked(parentDb) : false
const holder = locked ? getLockHolder(parentDb) : null
const entry = {
...store,
modified,
ageDays,
locked,
holder,
sizeFormatted: formatBytes(store.size)
}
if (locked) {
categories.active.push(entry)
} else if (ageDays > 180) {
categories.stale.push(entry)
} else if (store.path.includes('backup') || store.path.includes('broken') || store.path.includes('test') || store.path.includes('Videos')) {
categories.junk.push(entry)
} else {
categories.unknown.push(entry)
}
}
const printEntry = (e) => {
const lockStr = e.locked ? ` [LOCKED by ${e.holder?.command} PID ${e.holder?.pid}]` : ''
const ageStr = e.ageDays < 365 ? `${e.ageDays}d ago` : `${Math.floor(e.ageDays / 365)}y ago`
console.log(` ${e.sizeFormatted.padStart(10)} ${ageStr.padStart(8)} ${e.path}${lockStr}`)
}
if (categories.active.length > 0) {
console.log('ACTIVE (locked by running processes):')
categories.active.forEach(printEntry)
console.log()
}
if (categories.unknown.length > 0) {
console.log('NEEDS REVIEW (unlocked, relatively recent):')
categories.unknown.forEach(printEntry)
console.log()
}
if (categories.stale.length > 0) {
console.log('STALE (>6 months old, unlocked):')
categories.stale.forEach(printEntry)
console.log()
}
if (categories.junk.length > 0) {
console.log('LIKELY JUNK (backups, test data, broken):')
categories.junk.forEach(printEntry)
console.log()
}
console.log(`Total: ${formatBytes(totalSize)} across ${stores.length} locations`)
const staleSize = categories.stale.reduce((s, e) => s + e.size, 0)
const junkSize = categories.junk.reduce((s, e) => s + e.size, 0)
console.log(`Potentially reclaimable: ${formatBytes(staleSize + junkSize)} (stale + junk)`)
}
const KEY_TYPES_LABEL = {
0x00: 'seed',
0x01: 'core-key',
0x02: 'dk',
0x03: 'info',
0x04: 'data'
}
if (command === 'extract') {
const storagePath = resolve(args[1] || '')
const readOnly = args.includes('--readonly')
const dryRun = args.includes('--dry-run')
const dpIdx = args.indexOf('--data-ptr')
const dataPtr = dpIdx >= 0 ? parseInt(args[dpIdx + 1]) : NaN
const outIdx = args.indexOf('--out')
const outDir = outIdx >= 0 ? resolve(args[outIdx + 1]) : null
const filterIdx = args.indexOf('--filter')
const filter = filterIdx >= 0 ? args[filterIdx + 1] : null
const maxIdx = args.indexOf('--max-bytes')
const maxBytes = maxIdx >= 0 ? parseSize(args[maxIdx + 1]) : Infinity
const limitIdx = args.indexOf('--limit')
const limit = limitIdx >= 0 ? parseInt(args[limitIdx + 1]) : Infinity
const blobIdx = args.indexOf('--blob-ptr')
const blobDataPointer = blobIdx >= 0 ? parseInt(args[blobIdx + 1]) : null
if (!storagePath || isNaN(dataPtr) || (!outDir && !dryRun)) {
console.error('Usage: hyper-inspector extract <storage-path> --data-ptr <n> --out <dir> [--readonly] [--filter <s>] [--max-bytes <n>] [--dry-run]')
console.error(' --data-ptr: the metadata core dataPointer from "cores"/"usage" output')
process.exit(1)
}
console.log(`\nExtracting drive (dataPointer ${dataPtr}) from ${storagePath}`)
if (dryRun) console.log('(dry run — no files written)')
const res = await extractDrive(storagePath, dataPtr, outDir, { readOnly, filter, maxBytes, limit, dryRun, blobDataPointer })
if (res.note) console.log('Note: ' + res.note)
console.log(`Blobs core dataPointer: ${res.blobsDataPointer}`)
for (const f of res.files.slice(0, 200)) {
console.log(` ${String(f.status || '').padEnd(13)} ${formatBytes(f.bytes).padStart(10)} ${f.path}`)
}
console.log(`\n${dryRun ? 'Would write' : 'Wrote'} ${res.written} file(s), ${formatBytes(res.totalBytes)}${res.skipped ? `, skipped ${res.skipped}` : ''}`)
if (!dryRun && outDir) console.log(`Output: ${outDir}`)
}