OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-issues / examples / cli.js

230 lines · javascript ref:
#!/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) })