#!/usr/bin/env bare
// Load Node.js compatibility for Bare runtime
await import('bare-node-runtime/global')
/**
* migrate-to-unified.js — Export data from old Autobases, import into unified MetadataBase
*
* Uses the REAL old apply functions (scripts/old-apply/) to open old Autobases
* and read their materialized views. Writes v2 operations to a fresh MetadataBase.
*
* Usage:
* node scripts/migrate-to-unified.js [--storage /path/to/storage] [--force]
*
* Prerequisites:
* - config:metadataBase must be deleted from registry (forces fresh MetadataBase)
* - scripts/old-apply/ must contain the old apply + operations files
*/
import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import Autobase from 'autobase'
import path from 'bare-path'
import os from 'bare-os'
import { MetadataBase } from '../lib/metadata-base.js'
// Old apply functions (from git history, with real key schemas)
import { apply as identityApply } from './old-apply/identity-apply.js'
import { apply as namespaceApply } from './old-apply/namespace-apply.js'
import { apply as deployApply } from './old-apply/deploy-apply.js'
import { apply as repoApply } from './old-apply/repo-apply.js'
// Suppress crashes from corrupted adopted LinkedRepos
let _suppress = false
process.on('uncaughtException', (err) => {
if (_suppress) return
console.error('Uncaught:', err.message)
process.exit(1)
})
// Parse args
const args = process.argv.slice(2)
let storagePath = null
const forceRerun = args.includes('--force')
for (let i = 0; i < args.length; i++) {
if (args[i] === '--storage' && args[i + 1]) storagePath = args[i + 1]
}
if (!storagePath) {
storagePath = path.join(os.homedir(), '.config', 'pear-git', 'server-data')
}
console.log(`[Migration] Storage: ${storagePath}`)
const store = new Corestore(storagePath)
const registryCore = store.get({ name: 'registry' })
const registry = new Hyperbee(registryCore, { keyEncoding: 'utf-8', valueEncoding: 'json' })
await registry.ready()
// Check idempotency
const migrationDone = await registry.get('config:unifiedMigration')
if (migrationDone && !forceRerun) {
console.log(`[Migration] Already completed at ${migrationDone.value.completedAt}`)
console.log('[Migration] Pass --force to re-run')
await store.close()
process.exit(0)
}
// Ensure MetadataBase config is fresh (or doesn't exist yet)
const mbConfig = await registry.get('config:metadataBase')
if (mbConfig) {
console.log('[Migration] MetadataBase config exists — will use existing')
} else {
console.log('[Migration] No MetadataBase config — will create fresh')
}
// Open MetadataBase
const metadataBase = new MetadataBase({
storagePath: storagePath,
bootstrapKey: mbConfig?.value?.bootstrapKey || null
})
await metadataBase.ready()
// Persist new key if created fresh
if (!mbConfig) {
await registry.put('config:metadataBase', {
bootstrapKey: metadataBase.bootstrapKeyHex,
createdAt: Date.now()
})
console.log(`[Migration] Created fresh MetadataBase: ${metadataBase.bootstrapKeyHex.slice(0, 16)}`)
} else {
console.log(`[Migration] MetadataBase: ${metadataBase.bootstrapKeyHex.slice(0, 16)}`)
}
const stats = { users: 0, identities: 0, servers: 0, deploys: 0, groups: 0, peers: 0, grants: 0, invites: 0, requests: 0, issues: 0, comments: 0, labels: 0, repoWriters: 0, repos: 0, errors: 0, skipped: 0 }
// ── Helper: open old Autobase with REAL apply function ────────
async function openOldBase (namespace, bootstrapKey, applyFn, timeoutMs = 30000) {
if (!bootstrapKey) return null
const cs = store.namespace(namespace)
const keyBuf = typeof bootstrapKey === 'string' ? Buffer.from(bootstrapKey, 'hex') : bootstrapKey
const base = new Autobase(cs, keyBuf, {
open: (s) => new Hyperbee(s.get('view', { valueEncoding: 'json' }), {
keyEncoding: 'utf-8',
valueEncoding: 'json'
}),
apply: applyFn,
valueEncoding: 'binary'
})
await Promise.race([
base.ready(),
new Promise((_, rej) => setTimeout(() => rej(new Error('ready timeout')), timeoutMs))
])
await Promise.race([
base.update(),
new Promise((_, rej) => setTimeout(() => rej(new Error('update timeout')), timeoutMs))
])
return base
}
async function readAll (view, prefix) {
const results = []
for await (const e of view.createReadStream({ gte: prefix, lt: prefix + '\xff' })) {
results.push({ key: e.key, value: e.value })
}
return results
}
// ── 1. Identity Registry ──────────────────────────────────────
console.log('\n[Migration] Phase 1: Identity Registry')
const idKey = (await registry.get('config:identityRegistry'))?.value?.bootstrapKey
if (idKey) {
try {
const base = await openOldBase('identity-registry', idKey, identityApply)
for (const { value } of await readAll(base.view, 'user:')) {
try {
await metadataBase.append({ v: 2, ns: 'identity', type: 'create-user', userId: value.userId, username: value.username || null, name: value.name || 'Anonymous', email: value.email || null, profilePicture: value.profilePicture || null, globalPermissions: value.globalPermissions || ['read'], repos: value.repos || [{ name: '*', permissions: ['read'] }], timestamp: value.createdAt || Date.now() })
if (value.deactivatedAt) {
await metadataBase.append({ v: 2, ns: 'identity', type: 'deactivate-user', userId: value.userId, deactivatedBy: value.deactivatedBy || 'system', reason: value.deactivationReason || null, timestamp: value.deactivatedAt })
}
stats.users++
} catch (err) { stats.errors++; console.error(` user ${value.userId}: ${err.message}`) }
}
console.log(` Users: ${stats.users}`)
for (const { value } of await readAll(base.view, 'identity:')) {
try {
await metadataBase.append({ v: 2, ns: 'identity', type: 'create-identity', identityId: value.identityId, method: value.method, userId: value.userId || null, credentials: value.credentials || {}, timestamp: value.createdAt || Date.now() })
if (value.revoked) {
await metadataBase.append({ v: 2, ns: 'identity', type: 'revoke-identity', identityId: value.identityId, revokedBy: value.revokedBy || 'system', reason: value.revocationReason || 'migrated', timestamp: value.revokedAt || Date.now() })
}
stats.identities++
} catch (err) { stats.errors++; console.error(` identity ${value.identityId}: ${err.message}`) }
}
console.log(` Identities: ${stats.identities}`)
await base.close()
} catch (err) {
console.error(` Failed to open Identity Registry: ${err.message}`)
stats.errors++
}
} else { console.log(' No identity registry — skipping') }
// ── 2. Deploy Registry ────────────────────────────────────────
console.log('\n[Migration] Phase 2: Deploy Registry')
const deployKey = (await registry.get('config:deployRegistry'))?.value?.bootstrapKey
if (deployKey) {
try {
const base = await openOldBase('deploy-registry', deployKey, deployApply)
for (const { value } of await readAll(base.view, 'server:')) {
try {
await metadataBase.append({ v: 2, ns: 'deploy', type: 'register-server', serverKey: value.serverKey, instanceUrl: value.instanceUrl || null, hostname: value.hostname || null, timestamp: value.registeredAt || Date.now() })
stats.servers++
} catch (err) { stats.errors++ }
}
for (const { value } of await readAll(base.view, 'app:')) {
try {
await metadataBase.append({ v: 2, ns: 'deploy', type: 'deploy-complete', repoName: value.repoName, commitHash: value.commitHash, branch: value.branch, channel: value.channel || value.repoName, serverKey: value.deployedBy || null, timestamp: value.deployedAt || Date.now() })
stats.deploys++
} catch (err) { stats.errors++ }
}
console.log(` Servers: ${stats.servers}, Deploys: ${stats.deploys}`)
await base.close()
} catch (err) {
console.error(` Failed to open Deploy Registry: ${err.message}`)
stats.errors++
}
} else { console.log(' No deploy registry — skipping') }
// ── 3. Namespace Registry ─────────────────────────────────────
console.log('\n[Migration] Phase 3: Namespace Registry')
const nsKey = (await registry.get('config:namespaceRegistry'))?.value?.bootstrapKey
if (nsKey) {
try {
const base = await openOldBase('namespace-registry', nsKey, namespaceApply)
for (const { value } of await readAll(base.view, 'namespace:')) {
try {
await metadataBase.append({ v: 2, ns: 'group', type: 'create-namespace', name: value.name, nsType: value.type, displayName: value.displayName || value.name, description: value.description || '', userId: value.userId || null, members: value.members || [], visibility: value.visibility || 'public', timestamp: value.createdAt || Date.now() })
for (const repoName of (value.repos || [])) {
await metadataBase.append({ v: 2, ns: 'group', type: 'add-repo', namespaceName: value.name, repoName, timestamp: value.updatedAt || Date.now() })
}
stats.groups++
} catch (err) { stats.errors++; console.error(` group ${value.name}: ${err.message}`) }
}
for (const { key, value } of await readAll(base.view, 'writer:')) {
if (value.removedAt) continue
const peerId = key.slice('writer:'.length)
try {
await metadataBase.append({ v: 2, ns: 'group', type: 'add-writer', peerId, serverKey: value.serverKey, role: value.role || 'writer', addedBy: value.addedBy || null, timestamp: value.addedAt || Date.now() })
stats.peers++
} catch (err) { stats.errors++ }
}
for (const { value } of await readAll(base.view, 'grant:')) {
try {
await metadataBase.append({ v: 2, ns: 'group', type: 'create-grant', grantId: value.grantId, subjectType: value.subjectType, subjectId: value.subjectId, grantType: value.grantType, groupName: value.groupName || null, repoName: value.repoName || null, permission: value.permission, grantedBy: value.grantedBy || null, timestamp: value.grantedAt || Date.now() })
stats.grants++
} catch (err) { stats.errors++ }
}
for (const { value } of await readAll(base.view, 'invite:')) {
try {
await metadataBase.append({ v: 2, ns: 'group', type: 'create-invite', inviteId: value.inviteId, inviteType: value.inviteType, targetGroup: value.targetGroup || null, targetRepo: value.targetRepo || null, permission: value.permission, maxUses: value.maxUses || 1, createdBy: value.createdBy || null, expiresAt: value.expiresAt || null, timestamp: value.createdAt || Date.now() })
stats.invites++
} catch (err) { stats.errors++ }
}
console.log(` Groups: ${stats.groups}, Peers: ${stats.peers}, Grants: ${stats.grants}, Invites: ${stats.invites}`)
await base.close()
} catch (err) {
console.error(` Failed to open Namespace Registry: ${err.message}`)
stats.errors++
}
} else { console.log(' No namespace registry — skipping') }
// ── 4. LinkedRepos (per-repo issues) ──────────────────────────
console.log('\n[Migration] Phase 4: LinkedRepos')
const repoConfigs = []
for await (const e of registry.createReadStream({ gt: 'config:issueAutobase:', lt: 'config:issueAutobase:\xff' })) {
const repoName = e.key.slice('config:issueAutobase:'.length)
// Skip adopted/federated repos — their Autobases have corrupted signers
if (e.value.adoptedFrom) {
console.log(` Skipping ${repoName}: adopted from ${e.value.adoptedFrom.slice(0, 8)} (corrupted signers)`)
stats.skipped++
continue
}
repoConfigs.push({ repoName, bootstrapKey: e.value.bootstrapKey })
}
console.log(` Found ${repoConfigs.length} repo configs`)
for (const { repoName, bootstrapKey } of repoConfigs) {
let base
_suppress = true
try {
base = await openOldBase(`linked:${repoName}`, bootstrapKey, repoApply, 15000)
} catch (err) {
console.log(` Skipping ${repoName}: ${err.message}`)
stats.skipped++
_suppress = false
continue
}
_suppress = false
if (!base) { stats.skipped++; continue }
let ri = 0, rc = 0, rl = 0, rw = 0
for (const { key, value } of await readAll(base.view, 'writer:')) {
const userId = key.slice('writer:'.length)
try {
await metadataBase.append({ v: 2, ns: 'issue', type: 'add-writer', repoName, userId, serverKey: value.serverKey, identityKey: value.identityKey || null, proof: value.proof || null, role: value.role || 'writer', addedBy: value.addedBy || null, timestamp: value.addedAt || Date.now() })
rw++; stats.repoWriters++
} catch (err) { stats.errors++ }
}
for (const { key, value } of await readAll(base.view, 'label:')) {
const name = key.slice('label:'.length)
try {
await metadataBase.append({ v: 2, ns: 'issue', type: 'create-label', repoName, name, color: value.color || '#ccc', description: value.description || '', displayName: value.displayName || name })
rl++; stats.labels++
} catch (err) { stats.errors++ }
}
for (const { value } of await readAll(base.view, 'issue:')) {
try {
await metadataBase.append({ v: 2, ns: 'issue', type: 'create-issue', repoName, id: value.id, title: value.title, body: value.body || '', userId: value.author, status: value.status || 'open', labels: value.labels || [], images: value.images || [], timestamp: value.createdAt || Date.now() })
ri++; stats.issues++
} catch (err) { stats.errors++; console.error(` issue ${repoName}#${value.id}: ${err.message}`) }
}
for (const { key, value } of await readAll(base.view, 'comment:')) {
const parts = key.slice('comment:'.length).split(':')
const issueId = parseInt(parts[0], 10)
const commentId = parts.slice(1).join(':')
try {
await metadataBase.append({ v: 2, ns: 'issue', type: 'create-comment', repoName, issueId, commentId, body: value.body, userId: value.author, timestamp: value.createdAt || Date.now() })
rc++; stats.comments++
} catch (err) { stats.errors++ }
}
try { await base.close() } catch {}
if (ri + rc + rl + rw > 0) console.log(` ${repoName}: ${ri} issues, ${rc} comments, ${rl} labels, ${rw} writers`)
stats.repos++
}
// ── Write migration flag ──────────────────────────────────────
await registry.put('config:unifiedMigration', { completedAt: new Date().toISOString(), stats })
console.log('\n═══════════════════════════════════════════')
console.log(` Users: ${stats.users} Identities: ${stats.identities}`)
console.log(` Servers: ${stats.servers} Deploys: ${stats.deploys}`)
console.log(` Groups: ${stats.groups} Peers: ${stats.peers} Grants: ${stats.grants} Invites: ${stats.invites}`)
console.log(` Repos: ${stats.repos} Issues: ${stats.issues} Comments: ${stats.comments} Labels: ${stats.labels}`)
console.log(` Repo writers: ${stats.repoWriters} Skipped: ${stats.skipped} Errors: ${stats.errors}`)
console.log('═══════════════════════════════════════════')
await metadataBase.close()
await store.close()
console.log(stats.errors > 0 ? '\n⚠️ Completed with errors' : '\n✓ Migration complete')