OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-orgs / examples / cli.js

351 lines · javascript ref:
#!/usr/bin/env node
/**
 * Mini CLI example for hyper-orgs
 *
 * Usage:
 *   node examples/cli.js init                                  Create an orgs store
 *   node examples/cli.js info                                  Show key + counts + writers
 *   node examples/cli.js create <name> [group|user]            Create an org
 *   node examples/cli.js list [group|user|all]                 List orgs
 *   node examples/cli.js show <name>                           Show org details
 *   node examples/cli.js add-member <org> <userId> [role]      Add a member
 *   node examples/cli.js rm-member <org> <userId>              Remove a member
 *   node examples/cli.js add-resource <org> <resource>         Attach a resource
 *   node examples/cli.js rm-resource <org> <resource>          Detach a resource
 *   node examples/cli.js grant <subject> <org> [perm]          Grant a user access to an org
 *   node examples/cli.js invite <org> [perm]                   Create an invite
 *   node examples/cli.js request <org> <userId> [perm]         Request access
 *   node examples/cli.js check <userId> <resource>             Check access
 *   node examples/cli.js delete <name>                         Delete empty org
 *   node examples/cli.js demo                                  Run a full demo
 *
 * Storage: HYPER_ORGS_STORE env var (default: ./.hyper-orgs-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 { HyperOrgs } from '../index.js'

const storagePath = process.env.HYPER_ORGS_STORE || './.hyper-orgs-cli'
const actor = 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 orgs = new HyperOrgs(store, { namespace: 'orgs' })
  await orgs.ready()
  return { orgs, store }
}

async function close ({ orgs, store }) {
  await orgs.close()
  await store.close()
}

function printOrg (org) {
  console.log('Org: %s (%s)', org.name, org.type)
  console.log('  displayName: %s', org.displayName)
  if (org.description) console.log('  description: %s', org.description)
  console.log('  visibility:  %s', org.visibility)
  if (org.type === 'user' && org.userId) console.log('  userId:      %s', org.userId)
  if (org.members.length > 0) {
    console.log('  members:')
    for (const m of org.members) console.log('    - %s (%s)', m.userId, m.role)
  }
  if (org.resources.length > 0) {
    console.log('  resources: %s', org.resources.join(', '))
  }
}

async function main () {
  if (cmd === 'init') {
    const ctx = await open()
    console.log('HyperOrgs initialized at: %s', storagePath)
    console.log('  key:      %s', ctx.orgs.keyHex)
    console.log('  localKey: %s', ctx.orgs.localKeyHex)
    console.log('  writable: %s', ctx.orgs.writable)
    await close(ctx)
    return
  }

  if (cmd === 'info') {
    const ctx = await open()
    const all = await ctx.orgs.list()
    const writers = await ctx.orgs.listWriters()
    const grants = await ctx.orgs.listGrants()
    const invites = await ctx.orgs.listInvites()
    const requests = await ctx.orgs.listAccessRequests()
    console.log('HyperOrgs: %s', storagePath)
    console.log('  key:        %s', ctx.orgs.keyHex)
    console.log('  localKey:   %s', ctx.orgs.localKeyHex)
    console.log('  writable:   %s', ctx.orgs.writable)
    console.log('  orgs:       %d', all.length)
    console.log('  writers:    %d', writers.length)
    console.log('  grants:     %d', grants.length)
    console.log('  invites:    %d', invites.length)
    console.log('  requests:   %d', requests.length)
    await close(ctx)
    return
  }

  if (cmd === 'create') {
    const name = args[1]
    const type = args[2] || 'group'
    if (!name) { console.error('Usage: cli.js create <name> [group|user]'); process.exit(1) }
    const ctx = await open()
    const org = await ctx.orgs.create(name, type, {
      userId: type === 'user' ? actor : null
    })
    console.log('Created %s "%s"', type, name)
    printOrg(org)
    await close(ctx)
    return
  }

  if (cmd === 'list') {
    const filter = args[1]
    const opts = {}
    if (filter === 'group' || filter === 'user') opts.type = filter
    const ctx = await open()
    const all = await ctx.orgs.list(opts)
    if (all.length === 0) {
      console.log('(no orgs)')
    } else {
      for (const o of all) {
        const icon = o.type === 'user' ? 'user' : 'group'
        const name = (o.name + ' '.repeat(20)).slice(0, 20)
        console.log('[%s] %s  %s', icon, name, o.displayName)
      }
      console.log('\n%d orgs', all.length)
    }
    await close(ctx)
    return
  }

  if (cmd === 'show') {
    const name = args[1]
    if (!name) { console.error('Usage: cli.js show <name>'); process.exit(1) }
    const ctx = await open()
    const org = await ctx.orgs.get(name)
    if (!org) {
      console.error('Org "%s" not found', name)
      await close(ctx)
      process.exit(1)
    }
    printOrg(org)
    await close(ctx)
    return
  }

  if (cmd === 'add-member') {
    const [_, org, userId, role = 'member'] = args
    if (!org || !userId) { console.error('Usage: cli.js add-member <org> <userId> [role]'); process.exit(1) }
    const ctx = await open()
    const updated = await ctx.orgs.addMember(org, userId, role)
    console.log('Added %s as %s to %s', userId, role, org)
    printOrg(updated)
    await close(ctx)
    return
  }

  if (cmd === 'rm-member') {
    const [_, org, userId] = args
    if (!org || !userId) { console.error('Usage: cli.js rm-member <org> <userId>'); process.exit(1) }
    const ctx = await open()
    await ctx.orgs.removeMember(org, userId)
    console.log('Removed %s from %s', userId, org)
    await close(ctx)
    return
  }

  if (cmd === 'add-resource') {
    const [_, org, resource] = args
    if (!org || !resource) { console.error('Usage: cli.js add-resource <org> <resource>'); process.exit(1) }
    const ctx = await open()
    const updated = await ctx.orgs.addResource(org, resource)
    console.log('Added resource %s to %s', resource, org)
    printOrg(updated)
    await close(ctx)
    return
  }

  if (cmd === 'rm-resource') {
    const [_, org, resource] = args
    if (!org || !resource) { console.error('Usage: cli.js rm-resource <org> <resource>'); process.exit(1) }
    const ctx = await open()
    await ctx.orgs.removeResource(org, resource)
    console.log('Removed resource %s from %s', resource, org)
    await close(ctx)
    return
  }

  if (cmd === 'grant') {
    const [_, subject, org, permission = 'write'] = args
    if (!subject || !org) { console.error('Usage: cli.js grant <subject> <org> [perm]'); process.exit(1) }
    const ctx = await open()
    const grant = await ctx.orgs.createGrant({
      subjectType: 'user',
      subjectId: subject,
      grantType: 'group',
      groupName: org,
      permission,
      grantedBy: actor
    })
    console.log('Grant %s: %s → %s (%s)', grant.grantId, subject, org, permission)
    await close(ctx)
    return
  }

  if (cmd === 'invite') {
    const [_, org, permission = 'write'] = args
    if (!org) { console.error('Usage: cli.js invite <org> [perm]'); process.exit(1) }
    const ctx = await open()
    const invite = await ctx.orgs.createInvite({
      inviteType: 'group',
      targetGroup: org,
      permission,
      maxUses: 1,
      createdBy: actor
    })
    console.log('Invite %s: %s (%s, 1 use)', invite.inviteId, org, permission)
    console.log('Share this inviteId with the target subject.')
    await close(ctx)
    return
  }

  if (cmd === 'request') {
    const [_, org, userId, permission = 'write'] = args
    if (!org || !userId) { console.error('Usage: cli.js request <org> <userId> [perm]'); process.exit(1) }
    const ctx = await open()
    const req = await ctx.orgs.createAccessRequest({
      subjectType: 'user',
      subjectId: userId,
      requestType: 'group',
      groupName: org,
      permission
    })
    console.log('Request %s: %s → %s (%s) status=%s', req.requestId, userId, org, permission, req.status)
    await close(ctx)
    return
  }

  if (cmd === 'check') {
    const [_, userId, resource] = args
    if (!userId || !resource) { console.error('Usage: cli.js check <userId> <resource>'); process.exit(1) }
    const ctx = await open()
    const allowed = await ctx.orgs.checkAccess({
      userId,
      resourceName: resource,
      requiredPermission: 'write'
    })
    console.log('%s can write %s? %s', userId, resource, allowed ? 'YES' : 'NO')
    await close(ctx)
    return
  }

  if (cmd === 'delete') {
    const name = args[1]
    if (!name) { console.error('Usage: cli.js delete <name>'); process.exit(1) }
    const ctx = await open()
    await ctx.orgs.delete(name)
    console.log('Deleted %s', name)
    await close(ctx)
    return
  }

  if (cmd === 'demo') {
    const ctx = await open()
    console.log('--- hyper-orgs demo ---\n')

    console.log('1. Create a user org and a group org')
    await ctx.orgs.create('alice', 'user', { userId: 'alice-key-hex' })
    await ctx.orgs.create('my-team', 'group', {
      displayName: 'My Team',
      description: 'A test team'
    })

    console.log('2. Add members to the group')
    await ctx.orgs.addMember('my-team', 'alice', 'admin')
    await ctx.orgs.addMember('my-team', 'bob', 'member')

    console.log('3. Attach two resources to the team')
    await ctx.orgs.addResource('my-team', 'project-a')
    await ctx.orgs.addResource('my-team', 'project-b')

    console.log('4. Grant alice "write" on the whole team')
    await ctx.orgs.createGrant({
      subjectType: 'user', subjectId: 'alice',
      grantType: 'group', groupName: 'my-team',
      permission: 'write',
      grantedBy: 'demo'
    })

    console.log('5. Create an invite for anyone who wants to join')
    const invite = await ctx.orgs.createInvite({
      inviteType: 'group', targetGroup: 'my-team',
      permission: 'write', maxUses: 3,
      createdBy: 'demo'
    })
    console.log('   invite: %s', invite.inviteId)

    console.log('6. Someone requests access')
    const req = await ctx.orgs.createAccessRequest({
      subjectType: 'user', subjectId: 'carol',
      requestType: 'group', groupName: 'my-team',
      permission: 'write', reason: 'want to help'
    })

    console.log('7. Admin approves the request')
    await ctx.orgs.approveAccessRequest(req.requestId, 'demo')

    console.log('\n8. Summary:')
    const team = await ctx.orgs.get('my-team')
    printOrg(team)
    console.log()

    const allowed = await ctx.orgs.checkAccess({
      userId: 'alice',
      resourceName: 'project-a',
      requiredPermission: 'write'
    })
    console.log('alice can write project-a? %s', allowed ? 'YES' : 'NO')

    const carolAllowed = await ctx.orgs.checkAccess({
      userId: 'carol',
      resourceName: 'project-a',
      requiredPermission: 'write'
    })
    console.log('carol can write project-a? %s', carolAllowed ? 'YES' : 'NO')

    await close(ctx)
    return
  }

  console.log('hyper-orgs example CLI')
  console.log('  init                                  Create an orgs store')
  console.log('  info                                  Show key + counts + writers')
  console.log('  create <name> [group|user]            Create an org')
  console.log('  list [group|user|all]                 List orgs')
  console.log('  show <name>                           Show org details')
  console.log('  add-member <org> <userId> [role]      Add a member')
  console.log('  rm-member <org> <userId>              Remove a member')
  console.log('  add-resource <org> <resource>         Attach a resource')
  console.log('  rm-resource <org> <resource>          Detach a resource')
  console.log('  grant <subject> <org> [perm]          Grant access to an org')
  console.log('  invite <org> [perm]                   Create an invite')
  console.log('  request <org> <userId> [perm]         Request access')
  console.log('  check <userId> <resource>             Check access')
  console.log('  delete <name>                         Delete empty org')
  console.log('  demo                                  Run a full demo')
  console.log('')
  console.log('Storage: HYPER_ORGS_STORE env var (default: ./.hyper-orgs-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) })