#!/usr/bin/env node
/**
* Mini CLI example for hyper-issues
*
* Usage:
* node examples/cli.js init Create a tracker
* node examples/cli.js info Show key + writers + counts
* node examples/cli.js create <title> [-- body...] Create an issue
* node examples/cli.js list [open|closed|all] List issues
* node examples/cli.js show <id> Show issue + comments
* node examples/cli.js close <id> Close an issue
* node examples/cli.js reopen <id> Reopen an issue
* node examples/cli.js comment <id> <body...> Add a comment
* node examples/cli.js label <id> <name1,name2,...> Add labels
* node examples/cli.js init-labels Create default labels
* node examples/cli.js demo Run a full demo
*
* Storage: HYPER_ISSUES_STORE env var (default: ./.hyper-issues-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.
*/
import Corestore from 'corestore'
import { HyperIssues } from '../index.js'
const storagePath = process.env.HYPER_ISSUES_STORE || './.hyper-issues-cli'
const author = process.env.USER || 'me'
const args = process.argv.slice(2)
const cmd = args[0]
async function open () {
const store = new Corestore(storagePath)
await store.ready()
const tracker = new HyperIssues(store, { namespace: 'issues' })
await tracker.ready()
return { tracker, store }
}
async function close ({ tracker, store }) {
await tracker.close()
await store.close()
}
async function main () {
if (cmd === 'init') {
const ctx = await open()
console.log('Tracker initialized at: %s', storagePath)
console.log(' key: %s', ctx.tracker.keyHex)
console.log(' localKey: %s', ctx.tracker.localKeyHex)
console.log(' writable: %s', ctx.tracker.writable)
await close(ctx)
return
}
if (cmd === 'info') {
const ctx = await open()
const counts = await ctx.tracker.counts()
const writers = await ctx.tracker.listWriters()
const labels = await ctx.tracker.listLabels()
console.log('Tracker: %s', storagePath)
console.log(' key: %s', ctx.tracker.keyHex)
console.log(' localKey: %s', ctx.tracker.localKeyHex)
console.log(' writable: %s', ctx.tracker.writable)
console.log(' issues: %d open, %d closed', counts.open, counts.closed)
console.log(' writers: %d', writers.length)
console.log(' labels: %d', labels.length)
await close(ctx)
return
}
if (cmd === 'create') {
const dashIdx = args.indexOf('--')
const title = dashIdx > 1 ? args.slice(1, dashIdx).join(' ') : args.slice(1).join(' ')
const body = dashIdx > 1 ? args.slice(dashIdx + 1).join(' ') : ''
if (!title) { console.error('Usage: cli.js create <title> [-- body...]'); process.exit(1) }
const ctx = await open()
const id = await ctx.tracker.create({ title, body, author })
console.log('Created issue #%d: %s', id, title)
await close(ctx)
return
}
if (cmd === 'list') {
const status = args[1] || 'all'
const ctx = await open()
const issues = await ctx.tracker.list({ status, sort: 'newest' })
if (issues.length === 0) {
console.log('(no %s issues)', status)
} else {
for (const i of issues) {
const marker = i.status === 'closed' ? '[x]' : '[ ]'
const labels = (i.labels || []).map(l => '#' + l).join(' ')
console.log('%s #%-3d %s %s', marker, i.id, i.title, labels)
}
console.log('\n%d issues', issues.length)
}
await close(ctx)
return
}
if (cmd === 'show') {
const id = parseInt(args[1])
if (!id) { console.error('Usage: cli.js show <id>'); process.exit(1) }
const ctx = await open()
const issue = await ctx.tracker.get(id)
if (!issue) {
console.error('Issue #%d not found', id)
await close(ctx)
process.exit(1)
}
console.log('#%d %s [%s]', issue.id, issue.title, issue.status)
console.log('by %s · %s', issue.author, new Date(issue.createdAt).toISOString())
if ((issue.labels || []).length > 0) console.log('labels: %s', issue.labels.map(l => '#' + l).join(' '))
if (issue.body) console.log('\n%s', issue.body)
const comments = await ctx.tracker.getComments(id)
if (comments.length > 0) {
console.log('\n--- %d comments ---', comments.length)
for (const c of comments) {
console.log(' by %s · %s', c.author, new Date(c.createdAt).toISOString())
console.log(' %s\n', c.body)
}
}
await close(ctx)
return
}
if (cmd === 'close' || cmd === 'reopen') {
const id = parseInt(args[1])
if (!id) { console.error('Usage: cli.js ' + cmd + ' <id>'); process.exit(1) }
const ctx = await open()
const status = cmd === 'close' ? 'closed' : 'open'
await ctx.tracker.update(id, { status }, author)
console.log('Issue #%d → %s', id, status)
await close(ctx)
return
}
if (cmd === 'comment') {
const id = parseInt(args[1])
const body = args.slice(2).join(' ')
if (!id || !body) { console.error('Usage: cli.js comment <id> <body...>'); process.exit(1) }
const ctx = await open()
const cId = await ctx.tracker.comment(id, { body, author })
console.log('Comment %s added to #%d', cId.slice(0, 12), id)
await close(ctx)
return
}
if (cmd === 'label') {
const id = parseInt(args[1])
const labels = (args[2] || '').split(',').map(l => l.trim()).filter(Boolean)
if (!id || labels.length === 0) { console.error('Usage: cli.js label <id> <name1,name2,...>'); process.exit(1) }
const ctx = await open()
await ctx.tracker.addLabels(id, labels, author)
console.log('Added labels to #%d: %s', id, labels.join(', '))
await close(ctx)
return
}
if (cmd === 'init-labels') {
const ctx = await open()
const labels = await ctx.tracker.initializeDefaultLabels()
console.log('Initialized %d default labels:', labels.length)
for (const l of labels) console.log(' #%s', l.name)
await close(ctx)
return
}
if (cmd === 'demo') {
const ctx = await open()
console.log('--- hyper-issues demo ---\n')
console.log('1. Initialize default labels')
await ctx.tracker.initializeDefaultLabels()
console.log('2. Create three issues')
const id1 = await ctx.tracker.create({ title: 'Login button broken', body: 'Clicking does nothing', author: 'alice' })
const id2 = await ctx.tracker.create({ title: 'Add dark mode', body: 'It would be nice', author: 'bob' })
const id3 = await ctx.tracker.create({ title: 'Update README', author: 'alice' })
console.log('3. Add labels')
await ctx.tracker.addLabels(id1, ['bug'], 'alice')
await ctx.tracker.addLabels(id2, ['enhancement'], 'bob')
await ctx.tracker.addLabels(id3, ['documentation'], 'alice')
console.log('4. Comment on issue #1')
await ctx.tracker.comment(id1, { body: 'I can reproduce on Firefox 130', author: 'bob' })
await ctx.tracker.comment(id1, { body: 'Found the bug — fix incoming', author: 'carol' })
console.log('5. Close issue #3')
await ctx.tracker.update(id3, { status: 'closed' }, 'alice')
console.log('\n6. List all issues:')
const all = await ctx.tracker.list({ sort: 'oldest' })
for (const i of all) {
const marker = i.status === 'closed' ? '[x]' : '[ ]'
const labels = (i.labels || []).map(l => '#' + l).join(' ')
console.log(' %s #%d %s %s', marker, i.id, i.title, labels)
}
const counts = await ctx.tracker.counts()
console.log('\n%d open · %d closed · %d total', counts.open, counts.closed, counts.total)
await close(ctx)
return
}
console.log('hyper-issues example CLI')
console.log(' init Create a tracker')
console.log(' info Show key + counts + writers')
console.log(' create <title> [-- body...] Create an issue')
console.log(' list [open|closed|all] List issues')
console.log(' show <id> Show issue + comments')
console.log(' close <id> | reopen <id> Change status')
console.log(' comment <id> <body...> Add a comment')
console.log(' label <id> <name1,name2,...> Add labels')
console.log(' init-labels Create default labels')
console.log(' demo Run a full demo')
console.log('')
console.log('Storage: HYPER_ISSUES_STORE env var (default: ./.hyper-issues-cli)')
console.log('Author: USER env var (default: me)')
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) })