OverviewFilesCommitsIssuesDocsReleasesPeers

wirecore / lib / cli / index.js

339 lines · javascript ref:
#!/usr/bin/env node

/**
 * Wirecore CLI - Command-line interface for Wirecore
 *
 * Usage:
 *   fedicore init                          - Initialize Wirecore
 *   fedicore post "Hello world"            - Post a message
 *   fedicore timeline                      - View timeline
 *   fedicore follow <publicKey> [name]     - Follow a user
 *   fedicore unfollow <publicKey>          - Unfollow a user
 *   fedicore profile                       - View your profile
 *   fedicore profile <publicKey>           - View another user's profile
 *   fedicore whoami                        - Show your public key
 */

import { Wirecore } from "../core/wirecore.js"
import { stdin, stdout } from 'process'
import { createInterface } from 'readline'

const STORAGE_PATH = './data/cli'

// ANSI colors
const colors = {
  reset: '\x1b[0m',
  bright: '\x1b[1m',
  dim: '\x1b[2m',
  cyan: '\x1b[36m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  magenta: '\x1b[35m',
  gray: '\x1b[90m'
}

class WirecoreCLI {
  constructor() {
    this.fc = null
  }

  async run() {
    const args = process.argv.slice(2)
    const command = args[0]

    if (!command || command === 'help') {
      this.showHelp()
      return
    }

    // Commands that don't require init
    if (command === 'init') {
      await this.init()
      return
    }

    // All other commands require Wirecore to be initialized
    this.fc = new Wirecore(STORAGE_PATH)
    await this.fc.init()

    try {
      switch (command) {
        case 'post':
          await this.post(args.slice(1).join(' '))
          break
        case 'timeline':
          await this.timeline()
          break
        case 'follow':
          await this.follow(args[1], args[2])
          break
        case 'unfollow':
          await this.unfollow(args[1])
          break
        case 'profile':
          await this.profile(args[1])
          break
        case 'whoami':
          await this.whoami()
          break
        case 'like':
          await this.like(args[1], args[2])
          break
        case 'boost':
          await this.boost(args[1], args[2])
          break
        case 'reply':
          await this.reply(args[1], args[2], args.slice(3).join(' '))
          break
        case 'interactive':
        case 'i':
          await this.interactive()
          break
        default:
          console.log(colors.yellow + 'Unknown command:', command, colors.reset)
          this.showHelp()
      }
    } finally {
      await this.fc.close()
    }
  }

  showHelp() {
    console.log(`
${colors.bright}Wirecore CLI${colors.reset} - Decentralized Social Network

${colors.cyan}Commands:${colors.reset}
  init                              Initialize Wirecore
  post <text>                       Post a message
  timeline                          View your timeline
  follow <publicKey> [name]         Follow a user
  unfollow <publicKey>              Unfollow a user
  profile [publicKey]               View profile (yours or another user's)
  whoami                            Show your public key
  like <author> <sequence>          Like a post
  boost <author> <sequence>         Boost a post
  reply <author> <sequence> <text>  Reply to a post
  interactive                       Interactive mode
  help                              Show this help

${colors.cyan}Examples:${colors.reset}
  fedicore init
  fedicore post "Hello decentralized world!"
  fedicore follow BSY7/er4VJIu08o39NaRAiPY... Alice
  fedicore timeline
  fedicore interactive

${colors.gray}Learn more: https://github.com/your-repo/fedicore${colors.reset}
`)
  }

  async init() {
    console.log(colors.bright + 'Initializing Wirecore...' + colors.reset)

    const fc = new Wirecore(STORAGE_PATH)
    await fc.init()

    console.log(colors.green + '\n✓ Wirecore initialized!' + colors.reset)
    console.log('\nYour public key:')
    console.log(colors.cyan + fc.getPublicKey() + colors.reset)
    console.log('\nShare this key with others so they can follow you!')
    console.log('\nBackup your keypair:')
    console.log(colors.yellow + fc.getKeypair() + colors.reset)

    await fc.close()
  }

  async post(text) {
    if (!text) {
      console.log(colors.yellow + 'Please provide text to post' + colors.reset)
      return
    }

    const message = await this.fc.post({ text })
    console.log(colors.green + '✓ Posted!' + colors.reset)
    console.log(`Sequence: ${message.sequence}`)
  }

  async timeline() {
    console.log(colors.bright + 'Loading timeline...' + colors.reset + '\n')

    const posts = await this.fc.getTimeline({ limit: 20 })

    if (posts.length === 0) {
      console.log(colors.gray + 'No posts yet. Follow some users!' + colors.reset)
      return
    }

    for (const post of posts) {
      this.printPost(post)
    }

    console.log(colors.dim + `\n(${posts.length} posts)` + colors.reset)
  }

  async follow(publicKey, name = 'Anonymous') {
    if (!publicKey) {
      console.log(colors.yellow + 'Please provide a public key' + colors.reset)
      return
    }

    await this.fc.follow(publicKey, name)
    console.log(colors.green + `✓ Now following ${name}` + colors.reset)
  }

  async unfollow(publicKey) {
    if (!publicKey) {
      console.log(colors.yellow + 'Please provide a public key' + colors.reset)
      return
    }

    await this.fc.unfollow(publicKey)
    console.log(colors.green + '✓ Unfollowed' + colors.reset)
  }

  async profile(publicKey) {
    const key = publicKey || this.fc.getPublicKey()
    const profile = await this.fc.getProfile(key)

    console.log('\n' + colors.bright + profile.name + colors.reset)
    console.log(colors.gray + profile.publicKey.substring(0, 16) + '...' + colors.reset)
    if (profile.bio) {
      console.log('\n' + profile.bio)
    }
    console.log('\n' + colors.dim + `${profile.posts} posts` + colors.reset + '\n')
  }

  async whoami() {
    console.log('\nYour public key:')
    console.log(colors.cyan + this.fc.getPublicKey() + colors.reset + '\n')
  }

  async like(author, sequence) {
    if (!author || !sequence) {
      console.log(colors.yellow + 'Usage: fedicore like <author> <sequence>' + colors.reset)
      return
    }
    await this.fc.like(author, parseInt(sequence))
    console.log(colors.green + '✓ Liked!' + colors.reset)
  }

  async boost(author, sequence) {
    if (!author || !sequence) {
      console.log(colors.yellow + 'Usage: fedicore boost <author> <sequence>' + colors.reset)
      return
    }
    await this.fc.boost(author, parseInt(sequence))
    console.log(colors.green + '✓ Boosted!' + colors.reset)
  }

  async reply(author, sequence, text) {
    if (!author || !sequence || !text) {
      console.log(colors.yellow + 'Usage: fedicore reply <author> <sequence> <text>' + colors.reset)
      return
    }
    await this.fc.reply(author, parseInt(sequence), text)
    console.log(colors.green + '✓ Replied!' + colors.reset)
  }

  async interactive() {
    console.log(colors.bright + 'Wirecore Interactive Mode' + colors.reset)
    console.log(colors.dim + 'Type "help" for commands, "exit" to quit\n' + colors.reset)

    const rl = createInterface({
      input: stdin,
      output: stdout,
      prompt: colors.cyan + 'fedicore> ' + colors.reset
    })

    rl.prompt()

    rl.on('line', async (line) => {
      const args = line.trim().split(' ')
      const cmd = args[0]

      try {
        switch (cmd) {
          case '':
            break
          case 'help':
            console.log('Commands: post, timeline, follow, unfollow, profile, whoami, exit')
            break
          case 'post':
            await this.post(args.slice(1).join(' '))
            break
          case 'timeline':
            await this.timeline()
            break
          case 'follow':
            await this.follow(args[1], args[2])
            break
          case 'unfollow':
            await this.unfollow(args[1])
            break
          case 'profile':
            await this.profile(args[1])
            break
          case 'whoami':
            await this.whoami()
            break
          case 'exit':
          case 'quit':
            console.log(colors.green + 'Goodbye!' + colors.reset)
            await this.fc.close()
            rl.close()
            return
          default:
            console.log(colors.yellow + 'Unknown command:', cmd, colors.reset)
        }
      } catch (err) {
        console.error(colors.yellow + 'Error:', err.message, colors.reset)
      }

      rl.prompt()
    })

    rl.on('close', async () => {
      await this.fc.close()
      process.exit(0)
    })
  }

  printPost(post) {
    const authorShort = post.author.substring(0, 8) + '...'
    const date = new Date(post.timestamp).toLocaleString()
    const type = post.content.type

    // Header
    console.log(colors.cyan + authorShort + colors.reset +
                colors.gray + ' • ' + date + colors.reset)

    // Content
    switch (type) {
      case 'post':
        console.log(post.content.text)
        break
      case 'reply':
        console.log(colors.dim + 'Reply to ' + post.content.target.author.substring(0, 8) + '...' + colors.reset)
        console.log(post.content.text)
        break
      case 'boost':
        console.log(colors.green + '↻ Boosted ' + post.content.target.author.substring(0, 8) + '...' + colors.reset)
        break
      case 'like':
        console.log(colors.magenta + '♥ Liked ' + post.content.target.author.substring(0, 8) + '...' + colors.reset)
        break
    }

    console.log(colors.dim + `[seq: ${post.sequence}]` + colors.reset)
    console.log('')
  }
}

// Run CLI
const cli = new WirecoreCLI()
cli.run().catch(err => {
  console.error(colors.yellow + 'Error:', err.message, colors.reset)
  process.exit(1)
})