#!/usr/bin/env node
/**
* Mini CLI example for hyper-rooms
*
* Demonstrates a Room with a tiny "todos" schema. Lets you create rooms,
* append todos, list them, and manage writers.
*
* Usage:
* node examples/cli.js init Create a new room
* node examples/cli.js info Show room key + writers
* node examples/cli.js add <text> Add a todo
* node examples/cli.js list List todos
* node examples/cli.js check <id> Mark todo done
* node examples/cli.js del <id> Delete todo
* node examples/cli.js add-writer <userId> <writerKey> Add another writer
* node examples/cli.js list-writers List writers
* node examples/cli.js demo Run a full demo
*
* Storage: HYPER_ROOMS_STORE env var (default: ./.hyper-rooms-cli)
*
* NOTE: this CLI uses its own storage path. The web example uses a different
* default. They don't share state — RocksDB single-writer lock. For shared
* state across CLI/web/native interfaces in production, embed hyper-rooms in
* a single daemon and have all clients talk to the daemon via hyper-svc
* (planned).
*/
import Corestore from 'corestore'
import { Room, jsonSchema } from '../index.js'
const storagePath = process.env.HYPER_ROOMS_STORE || './.hyper-rooms-cli'
const args = process.argv.slice(2)
const cmd = args[0]
const todosSchema = jsonSchema({
async apply (batch, op, host) {
if (op.type === 'add-todo') {
await batch.put(`todo:${op.id}`, {
id: op.id,
text: op.text,
done: false,
createdAt: op.timestamp
})
// Bump the counter
const counter = await batch.get('meta:next-id')
await batch.put('meta:next-id', { value: Math.max(counter?.value?.value || 1, op.id + 1) })
} else if (op.type === 'check-todo') {
const e = await batch.get(`todo:${op.id}`)
if (e) await batch.put(`todo:${op.id}`, { ...e.value, done: true })
} else if (op.type === 'delete-todo') {
await batch.del(`todo:${op.id}`)
}
}
})
async function open () {
const store = new Corestore(storagePath)
await store.ready()
const room = new Room(store, { schema: todosSchema, namespace: 'todos' })
await room.ready()
return { room, store }
}
async function close ({ room, store }) {
await room.close()
await store.close()
}
async function nextId (room) {
const counter = await room.view.get('meta:next-id')
return counter ? counter.value.value : 1
}
async function main () {
if (cmd === 'init') {
const ctx = await open()
console.log('Room initialized at: %s', storagePath)
console.log(' key: %s', ctx.room.keyHex)
console.log(' localKey: %s', ctx.room.localKeyHex)
console.log(' writable: %s', ctx.room.writable)
await close(ctx)
return
}
if (cmd === 'info') {
const ctx = await open()
console.log('Room: %s', storagePath)
console.log(' key: %s', ctx.room.keyHex)
console.log(' localKey: %s', ctx.room.localKeyHex)
console.log(' writable: %s', ctx.room.writable)
const writers = await ctx.room.listWriters()
console.log(' writers: %d', writers.length)
for (const w of writers) {
console.log(' %s (%s) %s', w.userId, w.role, (w.writerKey || '').slice(0, 16) + '...')
}
await close(ctx)
return
}
if (cmd === 'add') {
const text = args.slice(1).join(' ')
if (!text) { console.error('Usage: cli.js add <text>'); process.exit(1) }
const ctx = await open()
if (!ctx.room.writable) {
console.error('Room is not writable from this peer (no writer key registered).')
await close(ctx)
process.exit(1)
}
const id = await nextId(ctx.room)
await ctx.room.append({ type: 'add-todo', id, text, timestamp: Date.now() })
await ctx.room.base.update()
console.log('Added todo #%d: %s', id, text)
await close(ctx)
return
}
if (cmd === 'list') {
const ctx = await open()
const todos = []
for await (const entry of ctx.room.view.createReadStream({
gte: 'todo:', lt: 'todo:\xff'
})) {
todos.push(entry.value)
}
if (todos.length === 0) {
console.log('(no todos)')
} else {
for (const t of todos) {
console.log(' [%s] #%d %s', t.done ? 'x' : ' ', t.id, t.text)
}
console.log('\n%d todos', todos.length)
}
await close(ctx)
return
}
if (cmd === 'check') {
const id = parseInt(args[1])
if (!id) { console.error('Usage: cli.js check <id>'); process.exit(1) }
const ctx = await open()
await ctx.room.append({ type: 'check-todo', id })
await ctx.room.base.update()
console.log('Checked #%d', id)
await close(ctx)
return
}
if (cmd === 'del') {
const id = parseInt(args[1])
if (!id) { console.error('Usage: cli.js del <id>'); process.exit(1) }
const ctx = await open()
await ctx.room.append({ type: 'delete-todo', id })
await ctx.room.base.update()
console.log('Deleted #%d', id)
await close(ctx)
return
}
if (cmd === 'add-writer') {
const userId = args[1]
const writerKey = args[2]
if (!userId || !writerKey) {
console.error('Usage: cli.js add-writer <userId> <writerKey>')
process.exit(1)
}
const ctx = await open()
await ctx.room.addWriter(userId, { writerKey, role: 'writer' })
await ctx.room.base.update()
console.log('Added writer %s (%s)', userId, writerKey.slice(0, 16) + '...')
await close(ctx)
return
}
if (cmd === 'list-writers') {
const ctx = await open()
const writers = await ctx.room.listWriters()
if (writers.length === 0) {
console.log('(no writers)')
} else {
for (const w of writers) {
console.log(' %s (%s) writerKey=%s addedBy=%s', w.userId, w.role, (w.writerKey || '').slice(0, 16) + '...', w.addedBy || '?')
}
}
await close(ctx)
return
}
if (cmd === 'demo') {
const ctx = await open()
console.log('--- hyper-rooms todos demo ---\n')
console.log('Room key: %s', ctx.room.keyHex)
console.log('Local key: %s\n', ctx.room.localKeyHex)
console.log('1. Add three todos')
await ctx.room.append({ type: 'add-todo', id: 1, text: 'buy milk', timestamp: Date.now() })
await ctx.room.append({ type: 'add-todo', id: 2, text: 'walk dog', timestamp: Date.now() })
await ctx.room.append({ type: 'add-todo', id: 3, text: 'write code', timestamp: Date.now() })
await ctx.room.base.update()
console.log('2. Check off "walk dog"')
await ctx.room.append({ type: 'check-todo', id: 2 })
await ctx.room.base.update()
console.log('3. List todos:')
for await (const entry of ctx.room.view.createReadStream({ gte: 'todo:', lt: 'todo:\xff' })) {
const t = entry.value
console.log(' [%s] #%d %s', t.done ? 'x' : ' ', t.id, t.text)
}
await close(ctx)
return
}
console.log('hyper-rooms example CLI (todos schema)')
console.log(' init Create a new room')
console.log(' info Show room key + writers')
console.log(' add <text> Add a todo')
console.log(' list List todos')
console.log(' check <id> Mark todo done')
console.log(' del <id> Delete todo')
console.log(' add-writer <userId> <key> Authorize another writer')
console.log(' list-writers List authorized writers')
console.log(' demo Run a full demo')
console.log('')
console.log('Storage: HYPER_ROOMS_STORE env var (default: ./.hyper-rooms-cli)')
console.log('Note: this CLI does NOT share state with examples/web.js — they use')
console.log('different storage paths because RocksDB only allows one writer.')
}
main().catch((err) => { console.error(err.message); process.exit(1) })