mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-10-06 08:39:08 +00:00
This commit adds wallet-based authentication to protected CLI commands by removing the manual auth command and automatically prompting for credentials when needed. Protected commands will check for valid credentials and trigger the auth
798 lines
20 KiB
Go
798 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.debros.io/DeBros/network/pkg/anyoneproxy"
|
|
"git.debros.io/DeBros/network/pkg/auth"
|
|
"git.debros.io/DeBros/network/pkg/client"
|
|
"github.com/libp2p/go-libp2p/core/crypto"
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
)
|
|
|
|
var (
|
|
bootstrapPeer = "/ip4/127.0.0.1/tcp/4001"
|
|
timeout = 30 * time.Second
|
|
format = "table"
|
|
useProduction = false
|
|
disableAnon = false
|
|
)
|
|
|
|
// version metadata populated via -ldflags at build time
|
|
var (
|
|
version = "dev"
|
|
commit = ""
|
|
date = ""
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
showHelp()
|
|
return
|
|
}
|
|
|
|
command := os.Args[1]
|
|
args := os.Args[2:]
|
|
|
|
// Parse global flags
|
|
parseGlobalFlags(args)
|
|
|
|
// Apply disable flag early so all network operations honor it
|
|
anyoneproxy.SetDisabled(disableAnon)
|
|
|
|
switch command {
|
|
case "version":
|
|
fmt.Printf("network-cli %s", version)
|
|
if commit != "" {
|
|
fmt.Printf(" (commit %s)", commit)
|
|
}
|
|
if date != "" {
|
|
fmt.Printf(" built %s", date)
|
|
}
|
|
fmt.Println()
|
|
return
|
|
case "health":
|
|
handleHealth()
|
|
case "peers":
|
|
handlePeers()
|
|
case "status":
|
|
handleStatus()
|
|
case "query":
|
|
if len(args) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Usage: network-cli query <sql>\n")
|
|
os.Exit(1)
|
|
}
|
|
handleQuery(args[0])
|
|
case "storage":
|
|
handleStorage(args)
|
|
case "pubsub":
|
|
handlePubSub(args)
|
|
case "connect":
|
|
if len(args) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Usage: network-cli connect <peer_address>\n")
|
|
os.Exit(1)
|
|
}
|
|
handleConnect(args[0])
|
|
case "peer-id":
|
|
handlePeerID()
|
|
case "help", "--help", "-h":
|
|
showHelp()
|
|
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
|
|
showHelp()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func parseGlobalFlags(args []string) {
|
|
for i, arg := range args {
|
|
switch arg {
|
|
case "-b", "--bootstrap":
|
|
if i+1 < len(args) {
|
|
bootstrapPeer = args[i+1]
|
|
}
|
|
case "-f", "--format":
|
|
if i+1 < len(args) {
|
|
format = args[i+1]
|
|
}
|
|
case "-t", "--timeout":
|
|
if i+1 < len(args) {
|
|
if d, err := time.ParseDuration(args[i+1]); err == nil {
|
|
timeout = d
|
|
}
|
|
}
|
|
case "--production":
|
|
useProduction = true
|
|
case "--disable-anonrc":
|
|
disableAnon = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleHealth() {
|
|
client, err := createClient()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer client.Disconnect()
|
|
|
|
health, err := client.Health()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to get health: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if format == "json" {
|
|
printJSON(health)
|
|
} else {
|
|
printHealth(health)
|
|
}
|
|
}
|
|
|
|
func handlePeers() {
|
|
client, err := createClient()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer client.Disconnect()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
peers, err := client.Network().GetPeers(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to get peers: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if format == "json" {
|
|
printJSON(peers)
|
|
} else {
|
|
printPeers(peers)
|
|
}
|
|
}
|
|
|
|
func handleStatus() {
|
|
client, err := createClient()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer client.Disconnect()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
status, err := client.Network().GetStatus(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to get status: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if format == "json" {
|
|
printJSON(status)
|
|
} else {
|
|
printStatus(status)
|
|
}
|
|
}
|
|
|
|
func handleQuery(sql string) {
|
|
// Ensure user is authenticated
|
|
_ = ensureAuthenticated()
|
|
|
|
client, err := createClient()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer client.Disconnect()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
result, err := client.Database().Query(ctx, sql)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to execute query: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if format == "json" {
|
|
printJSON(result)
|
|
} else {
|
|
printQueryResult(result)
|
|
}
|
|
}
|
|
|
|
func handleStorage(args []string) {
|
|
if len(args) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Usage: network-cli storage <get|put|list> [args...]\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Ensure user is authenticated
|
|
_ = ensureAuthenticated()
|
|
|
|
client, err := createClient()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer client.Disconnect()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
subcommand := args[0]
|
|
switch subcommand {
|
|
case "get":
|
|
if len(args) < 2 {
|
|
fmt.Fprintf(os.Stderr, "Usage: network-cli storage get <key>\n")
|
|
os.Exit(1)
|
|
}
|
|
value, err := client.Storage().Get(ctx, args[1])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to get value: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Try to decode if it looks like base64
|
|
decoded := tryDecodeBase64(string(value))
|
|
fmt.Printf("%s\n", decoded)
|
|
|
|
case "put":
|
|
if len(args) < 3 {
|
|
fmt.Fprintf(os.Stderr, "Usage: network-cli storage put <key> <value>\n")
|
|
os.Exit(1)
|
|
}
|
|
err := client.Storage().Put(ctx, args[1], []byte(args[2]))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to store value: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("✅ Stored key: %s\n", args[1])
|
|
|
|
case "list":
|
|
prefix := ""
|
|
if len(args) > 1 {
|
|
prefix = args[1]
|
|
}
|
|
keys, err := client.Storage().List(ctx, prefix, 100)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to list keys: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if format == "json" {
|
|
printJSON(keys)
|
|
} else {
|
|
for _, key := range keys {
|
|
fmt.Println(key)
|
|
}
|
|
}
|
|
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown storage command: %s\n", subcommand)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func handlePubSub(args []string) {
|
|
if len(args) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub <publish|subscribe|topics> [args...]\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Ensure user is authenticated
|
|
_ = ensureAuthenticated()
|
|
|
|
client, err := createClient()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer client.Disconnect()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
subcommand := args[0]
|
|
switch subcommand {
|
|
case "publish":
|
|
if len(args) < 3 {
|
|
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub publish <topic> <message>\n")
|
|
os.Exit(1)
|
|
}
|
|
err := client.PubSub().Publish(ctx, args[1], []byte(args[2]))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to publish message: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("✅ Published message to topic: %s\n", args[1])
|
|
|
|
case "subscribe":
|
|
if len(args) < 2 {
|
|
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub subscribe <topic> [duration]\n")
|
|
os.Exit(1)
|
|
}
|
|
duration := 30 * time.Second
|
|
if len(args) > 2 {
|
|
if d, err := time.ParseDuration(args[2]); err == nil {
|
|
duration = d
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), duration)
|
|
defer cancel()
|
|
|
|
fmt.Printf("🔔 Subscribing to topic '%s' for %v...\n", args[1], duration)
|
|
|
|
messageHandler := func(topic string, data []byte) error {
|
|
fmt.Printf("📨 [%s] %s: %s\n", time.Now().Format("15:04:05"), topic, string(data))
|
|
return nil
|
|
}
|
|
|
|
err := client.PubSub().Subscribe(ctx, args[1], messageHandler)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to subscribe: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
<-ctx.Done()
|
|
fmt.Printf("✅ Subscription ended\n")
|
|
|
|
case "topics":
|
|
topics, err := client.PubSub().ListTopics(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to list topics: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if format == "json" {
|
|
printJSON(topics)
|
|
} else {
|
|
for _, topic := range topics {
|
|
fmt.Println(topic)
|
|
}
|
|
}
|
|
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown pubsub command: %s\n", subcommand)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// ensureAuthenticated ensures the user has valid credentials for the gateway
|
|
// Returns the credentials or exits the program on failure
|
|
func ensureAuthenticated() *auth.Credentials {
|
|
gatewayURL := auth.GetDefaultGatewayURL()
|
|
|
|
credentials, err := auth.GetOrPromptForCredentials(gatewayURL)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Authentication failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
return credentials
|
|
}
|
|
|
|
func openBrowser(target string) error {
|
|
cmds := [][]string{
|
|
{"xdg-open", target},
|
|
{"open", target},
|
|
{"cmd", "/c", "start", target},
|
|
}
|
|
for _, c := range cmds {
|
|
cmd := exec.Command(c[0], c[1:]...)
|
|
if err := cmd.Start(); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
log.Printf("Please open %s manually", target)
|
|
return nil
|
|
}
|
|
|
|
// getenvDefault returns env var or default if empty/undefined.
|
|
func getenvDefault(key, def string) string {
|
|
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func handleConnect(peerAddr string) {
|
|
client, err := createClient()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer client.Disconnect()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
err = client.Network().ConnectToPeer(ctx, peerAddr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to connect to peer: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("✅ Connected to peer: %s\n", peerAddr)
|
|
}
|
|
|
|
func handlePeerID() {
|
|
// Try to get peer ID from running network first
|
|
client, err := createClient()
|
|
if err == nil {
|
|
defer client.Disconnect()
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
if status, err := client.Network().GetStatus(ctx); err == nil {
|
|
if format == "json" {
|
|
printJSON(map[string]string{"peer_id": status.NodeID})
|
|
} else {
|
|
fmt.Printf("🆔 Peer ID: %s\n", status.NodeID)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// Fallback: try to extract from local identity files
|
|
identityPaths := []string{
|
|
"/opt/debros/data/node/identity.key",
|
|
"/opt/debros/data/bootstrap/identity.key",
|
|
"/opt/debros/keys/node/identity.key",
|
|
"./data/node/identity.key",
|
|
"./data/bootstrap/identity.key",
|
|
}
|
|
|
|
for _, path := range identityPaths {
|
|
if peerID := extractPeerIDFromFile(path); peerID != "" {
|
|
if format == "json" {
|
|
printJSON(map[string]string{"peer_id": peerID, "source": "local_identity"})
|
|
} else {
|
|
fmt.Printf("🆔 Peer ID: %s\n", peerID)
|
|
fmt.Printf("📂 Source: %s\n", path)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check peer.info files as last resort
|
|
peerInfoPaths := []string{
|
|
"/opt/debros/data/node/peer.info",
|
|
"/opt/debros/data/bootstrap/peer.info",
|
|
"./data/node/peer.info",
|
|
"./data/bootstrap/peer.info",
|
|
}
|
|
|
|
for _, path := range peerInfoPaths {
|
|
if data, err := os.ReadFile(path); err == nil {
|
|
multiaddr := strings.TrimSpace(string(data))
|
|
if peerID := extractPeerIDFromMultiaddr(multiaddr); peerID != "" {
|
|
if format == "json" {
|
|
printJSON(map[string]string{"peer_id": peerID, "source": "peer_info"})
|
|
} else {
|
|
fmt.Printf("🆔 Peer ID: %s\n", peerID)
|
|
fmt.Printf("📂 Source: %s\n", path)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "❌ Could not find peer ID. Make sure the node is running or identity files exist.\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
func createClient() (client.NetworkClient, error) {
|
|
config := client.DefaultClientConfig("network-cli")
|
|
|
|
// Check for existing credentials
|
|
creds, err := auth.GetValidCredentials()
|
|
if err != nil {
|
|
// No valid credentials found, trigger authentication flow
|
|
fmt.Printf("🔐 Authentication required for DeBros Network CLI\n")
|
|
fmt.Printf("💡 This will open your browser to authenticate with your wallet\n")
|
|
|
|
gatewayURL := auth.GetDefaultGatewayURL()
|
|
fmt.Printf("🌐 Gateway: %s\n\n", gatewayURL)
|
|
|
|
// Perform wallet authentication
|
|
newCreds, authErr := auth.PerformWalletAuthentication(gatewayURL)
|
|
if authErr != nil {
|
|
return nil, fmt.Errorf("authentication failed: %w", authErr)
|
|
}
|
|
|
|
// Save credentials
|
|
if saveErr := auth.SaveCredentialsForDefaultGateway(newCreds); saveErr != nil {
|
|
fmt.Printf("⚠️ Warning: failed to save credentials: %v\n", saveErr)
|
|
} else {
|
|
fmt.Printf("💾 Credentials saved to ~/.debros/credentials.json\n")
|
|
}
|
|
|
|
creds = newCreds
|
|
}
|
|
|
|
// Configure client with API key
|
|
config.APIKey = creds.APIKey
|
|
|
|
// Update last used time
|
|
creds.UpdateLastUsed()
|
|
auth.SaveCredentialsForDefaultGateway(creds) // Best effort save
|
|
|
|
networkClient, err := client.NewClient(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := networkClient.Connect(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return networkClient, nil
|
|
}
|
|
|
|
// discoverBootstrapPeer tries to find the bootstrap peer from saved peer info
|
|
func discoverBootstrapPeer() string {
|
|
// Look for peer info in common locations
|
|
peerInfoPaths := []string{
|
|
"./data/bootstrap/peer.info",
|
|
"./data/test-bootstrap/peer.info",
|
|
"/tmp/bootstrap-peer.info",
|
|
}
|
|
|
|
for _, path := range peerInfoPaths {
|
|
if data, err := os.ReadFile(path); err == nil {
|
|
peerAddr := strings.TrimSpace(string(data))
|
|
if peerAddr != "" {
|
|
// Only print discovery message in table format
|
|
if format != "json" {
|
|
fmt.Printf("🔍 Discovered bootstrap peer: %s\n", peerAddr)
|
|
}
|
|
return peerAddr
|
|
}
|
|
}
|
|
}
|
|
|
|
return "" // Return empty string if no peer info found
|
|
}
|
|
|
|
// tryDecodeBase64 attempts to decode a string as base64, returns original if not valid base64
|
|
func tryDecodeBase64(s string) string {
|
|
// Only try to decode if it looks like base64 (no spaces, reasonable length)
|
|
if len(s) > 0 && len(s)%4 == 0 && !strings.ContainsAny(s, " \n\r\t") {
|
|
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
|
|
// Check if decoded result looks like readable text
|
|
decodedStr := string(decoded)
|
|
if isPrintableText(decodedStr) {
|
|
return decodedStr
|
|
}
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// isPrintableText checks if a string contains mostly printable characters
|
|
func isPrintableText(s string) bool {
|
|
printableCount := 0
|
|
for _, r := range s {
|
|
if r >= 32 && r <= 126 || r == '\n' || r == '\r' || r == '\t' {
|
|
printableCount++
|
|
}
|
|
}
|
|
return len(s) > 0 && float64(printableCount)/float64(len(s)) > 0.8
|
|
}
|
|
|
|
func showHelp() {
|
|
fmt.Printf("Network CLI - Distributed P2P Network Management Tool\n\n")
|
|
fmt.Printf("Usage: network-cli <command> [args...]\n\n")
|
|
fmt.Printf("🔐 Authentication: Commands requiring authentication will automatically prompt for wallet connection.\n\n")
|
|
fmt.Printf("Commands:\n")
|
|
fmt.Printf(" health - Check network health\n")
|
|
fmt.Printf(" peers - List connected peers\n")
|
|
fmt.Printf(" status - Show network status\n")
|
|
fmt.Printf(" peer-id - Show this node's peer ID\n")
|
|
fmt.Printf(" query <sql> 🔐 Execute database query\n")
|
|
fmt.Printf(" storage get <key> 🔐 Get value from storage\n")
|
|
fmt.Printf(" storage put <key> <value> 🔐 Store value in storage\n")
|
|
fmt.Printf(" storage list [prefix] 🔐 List storage keys\n")
|
|
fmt.Printf(" pubsub publish <topic> <msg> 🔐 Publish message\n")
|
|
fmt.Printf(" pubsub subscribe <topic> [duration] 🔐 Subscribe to topic\n")
|
|
fmt.Printf(" pubsub topics 🔐 List topics\n")
|
|
fmt.Printf(" connect <peer_address> - Connect to peer\n")
|
|
|
|
fmt.Printf(" help - Show this help\n\n")
|
|
fmt.Printf("Global Flags:\n")
|
|
fmt.Printf(" -b, --bootstrap <addr> - Bootstrap peer address (default: /ip4/127.0.0.1/tcp/4001)\n")
|
|
fmt.Printf(" -f, --format <format> - Output format: table, json (default: table)\n")
|
|
fmt.Printf(" -t, --timeout <duration> - Operation timeout (default: 30s)\n")
|
|
fmt.Printf(" --production - Connect to production bootstrap peers\n\n")
|
|
fmt.Printf("Authentication:\n")
|
|
fmt.Printf(" Commands marked with 🔐 will automatically prompt for wallet authentication\n")
|
|
fmt.Printf(" if no valid credentials are found. You can manage multiple wallets and\n")
|
|
fmt.Printf(" choose between them during the authentication flow.\n\n")
|
|
fmt.Printf("Examples:\n")
|
|
fmt.Printf(" network-cli health\n")
|
|
fmt.Printf(" network-cli peer-id\n")
|
|
fmt.Printf(" network-cli peer-id --format json\n")
|
|
fmt.Printf(" network-cli peers --format json\n")
|
|
fmt.Printf(" network-cli peers --production\n")
|
|
fmt.Printf(" network-cli storage put user:123 '{\"name\":\"Alice\"}'\n")
|
|
fmt.Printf(" network-cli pubsub subscribe notifications 1m\n")
|
|
}
|
|
|
|
// Print functions
|
|
|
|
func printHealth(health *client.HealthStatus) {
|
|
fmt.Printf("🏥 Network Health\n")
|
|
fmt.Printf("Status: %s\n", getStatusEmoji(health.Status)+health.Status)
|
|
fmt.Printf("Last Updated: %s\n", health.LastUpdated.Format("2006-01-02 15:04:05"))
|
|
fmt.Printf("Response Time: %v\n", health.ResponseTime)
|
|
fmt.Printf("\nChecks:\n")
|
|
for check, status := range health.Checks {
|
|
emoji := "✅"
|
|
if status != "ok" {
|
|
emoji = "❌"
|
|
}
|
|
fmt.Printf(" %s %s: %s\n", emoji, check, status)
|
|
}
|
|
}
|
|
|
|
func printPeers(peers []client.PeerInfo) {
|
|
fmt.Printf("👥 Connected Peers (%d)\n\n", len(peers))
|
|
if len(peers) == 0 {
|
|
fmt.Printf("No peers connected\n")
|
|
return
|
|
}
|
|
|
|
for i, peer := range peers {
|
|
connEmoji := "🔴"
|
|
if peer.Connected {
|
|
connEmoji = "🟢"
|
|
}
|
|
fmt.Printf("%d. %s %s\n", i+1, connEmoji, peer.ID)
|
|
fmt.Printf(" Addresses: %v\n", peer.Addresses)
|
|
fmt.Printf(" Last Seen: %s\n", peer.LastSeen.Format("2006-01-02 15:04:05"))
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
func printStatus(status *client.NetworkStatus) {
|
|
fmt.Printf("🌐 Network Status\n")
|
|
fmt.Printf("Node ID: %s\n", status.NodeID)
|
|
fmt.Printf("Connected: %s\n", getBoolEmoji(status.Connected)+strconv.FormatBool(status.Connected))
|
|
fmt.Printf("Peer Count: %d\n", status.PeerCount)
|
|
fmt.Printf("Database Size: %s\n", formatBytes(status.DatabaseSize))
|
|
fmt.Printf("Uptime: %v\n", status.Uptime.Round(time.Second))
|
|
}
|
|
|
|
func printQueryResult(result *client.QueryResult) {
|
|
fmt.Printf("📊 Query Result\n")
|
|
fmt.Printf("Rows: %d\n\n", result.Count)
|
|
|
|
if len(result.Rows) == 0 {
|
|
fmt.Printf("No data returned\n")
|
|
return
|
|
}
|
|
|
|
// Print header
|
|
for i, col := range result.Columns {
|
|
if i > 0 {
|
|
fmt.Printf(" | ")
|
|
}
|
|
fmt.Printf("%-15s", col)
|
|
}
|
|
fmt.Println()
|
|
|
|
// Print separator
|
|
for i := range result.Columns {
|
|
if i > 0 {
|
|
fmt.Printf("-+-")
|
|
}
|
|
fmt.Printf("%-15s", "---------------")
|
|
}
|
|
fmt.Println()
|
|
|
|
// Print rows
|
|
for _, row := range result.Rows {
|
|
for i, cell := range row {
|
|
if i > 0 {
|
|
fmt.Printf(" | ")
|
|
}
|
|
fmt.Printf("%-15v", cell)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
func printJSON(data interface{}) {
|
|
jsonData, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to marshal JSON: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Println(string(jsonData))
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func getStatusEmoji(status string) string {
|
|
switch status {
|
|
case "healthy":
|
|
return "🟢 "
|
|
case "degraded":
|
|
return "🟡 "
|
|
case "unhealthy":
|
|
return "🔴 "
|
|
default:
|
|
return "⚪ "
|
|
}
|
|
}
|
|
|
|
func getBoolEmoji(b bool) string {
|
|
if b {
|
|
return "✅ "
|
|
}
|
|
return "❌ "
|
|
}
|
|
|
|
func formatBytes(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
// extractPeerIDFromFile extracts peer ID from an identity key file
|
|
func extractPeerIDFromFile(keyFile string) string {
|
|
// Read the identity key file
|
|
data, err := os.ReadFile(keyFile)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
// Unmarshal the private key
|
|
priv, err := crypto.UnmarshalPrivateKey(data)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
// Get the public key
|
|
pub := priv.GetPublic()
|
|
|
|
// Get the peer ID
|
|
peerID, err := peer.IDFromPublicKey(pub)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return peerID.String()
|
|
}
|
|
|
|
// extractPeerIDFromMultiaddr extracts the peer ID from a multiaddr string
|
|
func extractPeerIDFromMultiaddr(multiaddr string) string {
|
|
// Look for /p2p/ followed by the peer ID
|
|
parts := strings.Split(multiaddr, "/p2p/")
|
|
if len(parts) >= 2 {
|
|
return parts[1]
|
|
}
|
|
return ""
|
|
}
|