import fs from 'bare-fs'
import path from 'bare-path'
const REGISTRY_FILE = 'identities.json'
const REGISTRY_VERSION = 1
export class IdentityRegistry {
constructor (baseDir) {
this.baseDir = baseDir
this.filePath = path.join(baseDir, REGISTRY_FILE)
this.data = { version: REGISTRY_VERSION, activeId: null, identities: [] }
}
async load () {
try {
const raw = await fs.promises.readFile(this.filePath, 'utf-8')
this.data = JSON.parse(raw)
} catch {
// No registry — run migration
await this._migrate()
}
return this.data
}
async save () {
const tmp = this.filePath + '.tmp'
await fs.promises.writeFile(tmp, JSON.stringify(this.data, null, 2), 'utf-8')
await fs.promises.rename(tmp, this.filePath)
}
list () {
return this.data.identities
}
get (id) {
return this.data.identities.find(e => e.id === id) || null
}
getActive () {
return this.get(this.data.activeId)
}
getActiveId () {
return this.data.activeId
}
async create ({ name }) {
const nextNum = this.data.identities.length
const id = 'identity-' + nextNum
const dir = path.join(this.baseDir, id)
await fs.promises.mkdir(dir, { recursive: true })
const entry = {
id,
name: name || id,
username: '',
dir,
created: new Date().toISOString()
}
this.data.identities.push(entry)
await this.save()
return entry
}
async setActive (id) {
if (!this.get(id)) throw new Error('Identity not found: ' + id)
this.data.activeId = id
await this.save()
}
async updateMeta (id, fields) {
const entry = this.get(id)
if (!entry) throw new Error('Identity not found: ' + id)
if (fields.name !== undefined) entry.name = fields.name
if (fields.username !== undefined) entry.username = fields.username
// The identity's home room (router home.get). Lives with the identity, so an
// identity switch or restore switches homes with it.
if (fields.homeRoomId !== undefined) entry.homeRoomId = fields.homeRoomId
await this.save()
return entry
}
async remove (id, deleteFiles) {
if (this.data.activeId === id) throw new Error('Cannot delete active identity')
const entry = this.get(id)
if (!entry) throw new Error('Identity not found: ' + id)
this.data.identities = this.data.identities.filter(e => e.id !== id)
await this.save()
if (deleteFiles && entry.dir) {
await fs.promises.rm(entry.dir, { recursive: true, force: true })
}
return { ok: true }
}
getStoragePath (id) {
const entry = this.get(id)
if (!entry) throw new Error('Identity not found: ' + id)
return entry.dir
}
async _migrate () {
const defaultDir = path.join(this.baseDir, 'default')
const targetId = 'identity-0'
const targetDir = path.join(this.baseDir, targetId)
let hasDefault = false
try {
await fs.promises.stat(defaultDir)
hasDefault = true
} catch {}
if (hasDefault) {
// Rename default/ -> identity-0/
try {
await fs.promises.rename(defaultDir, targetDir)
} catch (err) {
// If target already exists, default was already migrated
if (err.code !== 'ENOTEMPTY' && err.code !== 'EEXIST') throw err
}
this.data = {
version: REGISTRY_VERSION,
activeId: targetId,
identities: [{
id: targetId,
name: 'Default',
username: '',
dir: targetDir,
created: new Date().toISOString()
}]
}
} else {
// Check if identity-0 already exists (previous partial migration)
let hasTarget = false
try {
await fs.promises.stat(targetDir)
hasTarget = true
} catch {}
if (hasTarget) {
this.data = {
version: REGISTRY_VERSION,
activeId: targetId,
identities: [{
id: targetId,
name: 'Default',
username: '',
dir: targetDir,
created: new Date().toISOString()
}]
}
} else {
// Fresh install — create identity-0
await fs.promises.mkdir(targetDir, { recursive: true })
this.data = {
version: REGISTRY_VERSION,
activeId: targetId,
identities: [{
id: targetId,
name: 'Default',
username: '',
dir: targetDir,
created: new Date().toISOString()
}]
}
}
}
await this.save()
console.log('[identity-registry] Migration complete — active:', this.data.activeId)
}
}