network/cmd/cli/main.go
anonpenguin23 7c9851729e
chore: update .gitignore and CHANGELOG for new development features
- Added .dev/ directory to .gitignore to exclude development process files.
- Updated CHANGELOG.md with new entries for the one-command `make dev` target, full stack initialization, and improved configuration management.
- Simplified README instructions for generating configuration files and starting the complete network stack.
2025-10-25 08:19:31 +03:00

1340 lines
36 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/auth"
"github.com/DeBrosOfficial/network/pkg/client"
"github.com/DeBrosOfficial/network/pkg/config"
"github.com/DeBrosOfficial/network/pkg/encryption"
"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
)
// 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)
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 "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 "auth":
handleAuth(args)
case "config":
handleConfig(args)
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
}
}
}
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 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)
}
}
func handleAuth(args []string) {
if len(args) == 0 {
showAuthHelp()
return
}
subcommand := args[0]
switch subcommand {
case "login":
handleAuthLogin()
case "logout":
handleAuthLogout()
case "whoami":
handleAuthWhoami()
case "status":
handleAuthStatus()
default:
fmt.Fprintf(os.Stderr, "Unknown auth command: %s\n", subcommand)
showAuthHelp()
os.Exit(1)
}
}
func handleAuthLogin() {
gatewayURL := auth.GetDefaultGatewayURL()
fmt.Printf("🔐 Authenticating with gateway at: %s\n", gatewayURL)
// Use the wallet authentication flow
creds, err := auth.PerformWalletAuthentication(gatewayURL)
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Authentication failed: %v\n", err)
os.Exit(1)
}
// Save credentials to file
if err := auth.SaveCredentialsForDefaultGateway(creds); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to save credentials: %v\n", err)
os.Exit(1)
}
credsPath, _ := auth.GetCredentialsPath()
fmt.Printf("✅ Authentication successful!\n")
fmt.Printf("📁 Credentials saved to: %s\n", credsPath)
fmt.Printf("🎯 Wallet: %s\n", creds.Wallet)
fmt.Printf("🏢 Namespace: %s\n", creds.Namespace)
}
func handleAuthLogout() {
if err := auth.ClearAllCredentials(); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to clear credentials: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ Logged out successfully - all credentials have been cleared")
}
func handleAuthWhoami() {
store, err := auth.LoadCredentials()
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to load credentials: %v\n", err)
os.Exit(1)
}
gatewayURL := auth.GetDefaultGatewayURL()
creds, exists := store.GetCredentialsForGateway(gatewayURL)
if !exists || !creds.IsValid() {
fmt.Println("❌ Not authenticated - run 'network-cli auth login' to authenticate")
os.Exit(1)
}
fmt.Println("✅ Authenticated")
fmt.Printf(" Wallet: %s\n", creds.Wallet)
fmt.Printf(" Namespace: %s\n", creds.Namespace)
fmt.Printf(" Issued At: %s\n", creds.IssuedAt.Format("2006-01-02 15:04:05"))
if !creds.ExpiresAt.IsZero() {
fmt.Printf(" Expires At: %s\n", creds.ExpiresAt.Format("2006-01-02 15:04:05"))
}
if !creds.LastUsedAt.IsZero() {
fmt.Printf(" Last Used: %s\n", creds.LastUsedAt.Format("2006-01-02 15:04:05"))
}
if creds.Plan != "" {
fmt.Printf(" Plan: %s\n", creds.Plan)
}
}
func handleAuthStatus() {
store, err := auth.LoadCredentials()
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to load credentials: %v\n", err)
os.Exit(1)
}
gatewayURL := auth.GetDefaultGatewayURL()
creds, exists := store.GetCredentialsForGateway(gatewayURL)
fmt.Println("🔐 Authentication Status")
fmt.Printf(" Gateway URL: %s\n", gatewayURL)
if !exists || creds == nil {
fmt.Println(" Status: ❌ Not authenticated")
return
}
if !creds.IsValid() {
fmt.Println(" Status: ⚠️ Credentials expired")
if !creds.ExpiresAt.IsZero() {
fmt.Printf(" Expired At: %s\n", creds.ExpiresAt.Format("2006-01-02 15:04:05"))
}
return
}
fmt.Println(" Status: ✅ Authenticated")
fmt.Printf(" Wallet: %s\n", creds.Wallet)
fmt.Printf(" Namespace: %s\n", creds.Namespace)
if !creds.ExpiresAt.IsZero() {
fmt.Printf(" Expires: %s\n", creds.ExpiresAt.Format("2006-01-02 15:04:05"))
}
if !creds.LastUsedAt.IsZero() {
fmt.Printf(" Last Used: %s\n", creds.LastUsedAt.Format("2006-01-02 15:04:05"))
}
}
func showAuthHelp() {
fmt.Printf("🔐 Authentication Commands\n\n")
fmt.Printf("Usage: network-cli auth <subcommand>\n\n")
fmt.Printf("Subcommands:\n")
fmt.Printf(" login - Authenticate with wallet\n")
fmt.Printf(" logout - Clear stored credentials\n")
fmt.Printf(" whoami - Show current authentication status\n")
fmt.Printf(" status - Show detailed authentication info\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" network-cli auth login\n")
fmt.Printf(" network-cli auth whoami\n")
fmt.Printf(" network-cli auth status\n")
fmt.Printf(" network-cli auth logout\n\n")
fmt.Printf("Environment Variables:\n")
fmt.Printf(" DEBROS_GATEWAY_URL - Gateway URL (default: http://localhost:6001)\n")
}
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
}
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
}
}
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 using enhanced authentication
creds, err := auth.GetValidEnhancedCredentials()
if err != nil {
// No valid credentials found, use the enhanced authentication flow
gatewayURL := auth.GetDefaultGatewayURL()
newCreds, authErr := auth.GetOrPromptForCredentials(gatewayURL)
if authErr != nil {
return nil, fmt.Errorf("authentication failed: %w", authErr)
}
creds = newCreds
}
// Configure client with API key
config.APIKey = creds.APIKey
// Update last used time - the enhanced store handles saving automatically
creds.UpdateLastUsed()
networkClient, err := client.NewClient(config)
if err != nil {
return nil, err
}
if err := networkClient.Connect(); err != nil {
return nil, err
}
return networkClient, nil
}
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
}
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 handleConfig(args []string) {
if len(args) == 0 {
showConfigHelp()
return
}
subcommand := args[0]
subargs := args[1:]
switch subcommand {
case "init":
handleConfigInit(subargs)
case "validate":
handleConfigValidate(subargs)
case "help":
showConfigHelp()
default:
fmt.Fprintf(os.Stderr, "Unknown config subcommand: %s\n", subcommand)
showConfigHelp()
os.Exit(1)
}
}
func showConfigHelp() {
fmt.Printf("Config Management Commands\n\n")
fmt.Printf("Usage: network-cli config <subcommand> [options]\n\n")
fmt.Printf("Subcommands:\n")
fmt.Printf(" init - Generate full network stack in ~/.debros (bootstrap + 2 nodes + gateway)\n")
fmt.Printf(" validate --name <file> - Validate a config file\n\n")
fmt.Printf("Init Default Behavior (no --type):\n")
fmt.Printf(" Generates bootstrap.yaml, node2.yaml, node3.yaml, gateway.yaml with:\n")
fmt.Printf(" - Auto-generated identities for bootstrap, node2, node3\n")
fmt.Printf(" - Correct bootstrap_peers and join addresses\n")
fmt.Printf(" - Default ports: P2P 4001-4003, HTTP 5001-5003, Raft 7001-7003\n\n")
fmt.Printf("Init Options:\n")
fmt.Printf(" --type <type> - Single config type: node, bootstrap, gateway (skips stack generation)\n")
fmt.Printf(" --name <file> - Output filename (default: depends on --type or 'stack' for full stack)\n")
fmt.Printf(" --force - Overwrite existing config/stack files\n\n")
fmt.Printf("Single Config Options (with --type):\n")
fmt.Printf(" --id <id> - Node ID for bootstrap peers\n")
fmt.Printf(" --listen-port <port> - LibP2P listen port (default: 4001)\n")
fmt.Printf(" --rqlite-http-port <port> - RQLite HTTP port (default: 5001)\n")
fmt.Printf(" --rqlite-raft-port <port> - RQLite Raft port (default: 7001)\n")
fmt.Printf(" --join <host:port> - RQLite address to join (required for non-bootstrap)\n")
fmt.Printf(" --bootstrap-peers <peers> - Comma-separated bootstrap peer multiaddrs\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" network-cli config init # Generate full stack\n")
fmt.Printf(" network-cli config init --force # Overwrite existing stack\n")
fmt.Printf(" network-cli config init --type bootstrap # Single bootstrap config (legacy)\n")
fmt.Printf(" network-cli config validate --name node.yaml\n")
}
func handleConfigInit(args []string) {
// Parse flags
var (
cfgType = ""
name = "" // Will be set based on type if not provided
id string
listenPort = 4001
rqliteHTTPPort = 5001
rqliteRaftPort = 7001
joinAddr string
bootstrapPeers string
force bool
)
for i := 0; i < len(args); i++ {
switch args[i] {
case "--type":
if i+1 < len(args) {
cfgType = args[i+1]
i++
}
case "--name":
if i+1 < len(args) {
name = args[i+1]
i++
}
case "--id":
if i+1 < len(args) {
id = args[i+1]
i++
}
case "--listen-port":
if i+1 < len(args) {
if p, err := strconv.Atoi(args[i+1]); err == nil {
listenPort = p
}
i++
}
case "--rqlite-http-port":
if i+1 < len(args) {
if p, err := strconv.Atoi(args[i+1]); err == nil {
rqliteHTTPPort = p
}
i++
}
case "--rqlite-raft-port":
if i+1 < len(args) {
if p, err := strconv.Atoi(args[i+1]); err == nil {
rqliteRaftPort = p
}
i++
}
case "--join":
if i+1 < len(args) {
joinAddr = args[i+1]
i++
}
case "--bootstrap-peers":
if i+1 < len(args) {
bootstrapPeers = args[i+1]
i++
}
case "--force":
force = true
}
}
// If --type is not specified, generate full stack
if cfgType == "" {
initFullStack(force)
return
}
// Otherwise, continue with single-file generation
// Validate type
if cfgType != "node" && cfgType != "bootstrap" && cfgType != "gateway" {
fmt.Fprintf(os.Stderr, "Invalid --type: %s (expected: node, bootstrap, or gateway)\n", cfgType)
os.Exit(1)
}
// Set default name based on type if not provided
if name == "" {
switch cfgType {
case "bootstrap":
name = "bootstrap.yaml"
case "gateway":
name = "gateway.yaml"
default:
name = "node.yaml"
}
}
// Ensure config directory exists
configDir, err := config.EnsureConfigDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to ensure config directory: %v\n", err)
os.Exit(1)
}
configPath := filepath.Join(configDir, name)
// Check if file exists
if !force {
if _, err := os.Stat(configPath); err == nil {
fmt.Fprintf(os.Stderr, "Config file already exists at %s (use --force to overwrite)\n", configPath)
os.Exit(1)
}
}
// Generate config based on type
var configContent string
switch cfgType {
case "node":
configContent = generateNodeConfig(name, id, listenPort, rqliteHTTPPort, rqliteRaftPort, joinAddr, bootstrapPeers)
case "bootstrap":
configContent = generateBootstrapConfig(name, id, listenPort, rqliteHTTPPort, rqliteRaftPort)
case "gateway":
configContent = generateGatewayConfig(bootstrapPeers)
}
// Write config file
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write config file: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Configuration file created: %s\n", configPath)
fmt.Printf(" Type: %s\n", cfgType)
fmt.Printf("\nYou can now start the %s using the generated config.\n", cfgType)
}
func handleConfigValidate(args []string) {
var name string
for i := 0; i < len(args); i++ {
if args[i] == "--name" && i+1 < len(args) {
name = args[i+1]
i++
}
}
if name == "" {
fmt.Fprintf(os.Stderr, "Missing --name flag\n")
showConfigHelp()
os.Exit(1)
}
configDir, err := config.ConfigDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get config directory: %v\n", err)
os.Exit(1)
}
configPath := filepath.Join(configDir, name)
file, err := os.Open(configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open config file: %v\n", err)
os.Exit(1)
}
defer file.Close()
var cfg config.Config
if err := config.DecodeStrict(file, &cfg); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err)
os.Exit(1)
}
// Run validation
errs := cfg.Validate()
if len(errs) > 0 {
fmt.Fprintf(os.Stderr, "\n❌ Configuration errors (%d):\n", len(errs))
for _, err := range errs {
fmt.Fprintf(os.Stderr, " - %s\n", err)
}
os.Exit(1)
}
fmt.Printf("✅ Config is valid: %s\n", configPath)
}
func initFullStack(force bool) {
fmt.Printf("🚀 Initializing full network stack...\n")
// Ensure ~/.debros directory exists
homeDir, err := os.UserHomeDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get home directory: %v\n", err)
os.Exit(1)
}
debrosDir := filepath.Join(homeDir, ".debros")
if err := os.MkdirAll(debrosDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create ~/.debros directory: %v\n", err)
os.Exit(1)
}
// Step 1: Generate bootstrap identity
bootstrapIdentityDir := filepath.Join(debrosDir, "bootstrap")
bootstrapIdentityPath := filepath.Join(bootstrapIdentityDir, "identity.key")
if !force {
if _, err := os.Stat(bootstrapIdentityPath); err == nil {
fmt.Fprintf(os.Stderr, "Bootstrap identity already exists at %s (use --force to overwrite)\n", bootstrapIdentityPath)
os.Exit(1)
}
}
bootstrapInfo, err := encryption.GenerateIdentity()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to generate bootstrap identity: %v\n", err)
os.Exit(1)
}
if err := os.MkdirAll(bootstrapIdentityDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create bootstrap data directory: %v\n", err)
os.Exit(1)
}
if err := encryption.SaveIdentity(bootstrapInfo, bootstrapIdentityPath); err != nil {
fmt.Fprintf(os.Stderr, "Failed to save bootstrap identity: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Generated bootstrap identity: %s (Peer ID: %s)\n", bootstrapIdentityPath, bootstrapInfo.PeerID.String())
// Construct bootstrap multiaddr
bootstrapMultiaddr := fmt.Sprintf("/ip4/127.0.0.1/tcp/4001/p2p/%s", bootstrapInfo.PeerID.String())
fmt.Printf(" Bootstrap multiaddr: %s\n", bootstrapMultiaddr)
// Step 2: Generate bootstrap.yaml
bootstrapName := "bootstrap.yaml"
bootstrapPath := filepath.Join(debrosDir, bootstrapName)
if !force {
if _, err := os.Stat(bootstrapPath); err == nil {
fmt.Fprintf(os.Stderr, "Bootstrap config already exists at %s (use --force to overwrite)\n", bootstrapPath)
os.Exit(1)
}
}
bootstrapContent := generateBootstrapConfig(bootstrapName, "", 4001, 5001, 7001)
if err := os.WriteFile(bootstrapPath, []byte(bootstrapContent), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write bootstrap config: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Generated bootstrap config: %s\n", bootstrapPath)
// Step 3: Generate node2 identity and config
node2IdentityDir := filepath.Join(debrosDir, "node2")
node2IdentityPath := filepath.Join(node2IdentityDir, "identity.key")
if !force {
if _, err := os.Stat(node2IdentityPath); err == nil {
fmt.Fprintf(os.Stderr, "Node2 identity already exists at %s (use --force to overwrite)\n", node2IdentityPath)
os.Exit(1)
}
}
node2Info, err := encryption.GenerateIdentity()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to generate node2 identity: %v\n", err)
os.Exit(1)
}
if err := os.MkdirAll(node2IdentityDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create node2 data directory: %v\n", err)
os.Exit(1)
}
if err := encryption.SaveIdentity(node2Info, node2IdentityPath); err != nil {
fmt.Fprintf(os.Stderr, "Failed to save node2 identity: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Generated node2 identity: %s (Peer ID: %s)\n", node2IdentityPath, node2Info.PeerID.String())
node2Name := "node2.yaml"
node2Path := filepath.Join(debrosDir, node2Name)
if !force {
if _, err := os.Stat(node2Path); err == nil {
fmt.Fprintf(os.Stderr, "Node2 config already exists at %s (use --force to overwrite)\n", node2Path)
os.Exit(1)
}
}
node2Content := generateNodeConfig(node2Name, "", 4002, 5002, 7002, "127.0.0.1:7001", bootstrapMultiaddr)
if err := os.WriteFile(node2Path, []byte(node2Content), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write node2 config: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Generated node2 config: %s\n", node2Path)
// Step 4: Generate node3 identity and config
node3IdentityDir := filepath.Join(debrosDir, "node3")
node3IdentityPath := filepath.Join(node3IdentityDir, "identity.key")
if !force {
if _, err := os.Stat(node3IdentityPath); err == nil {
fmt.Fprintf(os.Stderr, "Node3 identity already exists at %s (use --force to overwrite)\n", node3IdentityPath)
os.Exit(1)
}
}
node3Info, err := encryption.GenerateIdentity()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to generate node3 identity: %v\n", err)
os.Exit(1)
}
if err := os.MkdirAll(node3IdentityDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create node3 data directory: %v\n", err)
os.Exit(1)
}
if err := encryption.SaveIdentity(node3Info, node3IdentityPath); err != nil {
fmt.Fprintf(os.Stderr, "Failed to save node3 identity: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Generated node3 identity: %s (Peer ID: %s)\n", node3IdentityPath, node3Info.PeerID.String())
node3Name := "node3.yaml"
node3Path := filepath.Join(debrosDir, node3Name)
if !force {
if _, err := os.Stat(node3Path); err == nil {
fmt.Fprintf(os.Stderr, "Node3 config already exists at %s (use --force to overwrite)\n", node3Path)
os.Exit(1)
}
}
node3Content := generateNodeConfig(node3Name, "", 4003, 5003, 7003, "127.0.0.1:7001", bootstrapMultiaddr)
if err := os.WriteFile(node3Path, []byte(node3Content), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write node3 config: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Generated node3 config: %s\n", node3Path)
// Step 5: Generate gateway.yaml
gatewayName := "gateway.yaml"
gatewayPath := filepath.Join(debrosDir, gatewayName)
if !force {
if _, err := os.Stat(gatewayPath); err == nil {
fmt.Fprintf(os.Stderr, "Gateway config already exists at %s (use --force to overwrite)\n", gatewayPath)
os.Exit(1)
}
}
gatewayContent := generateGatewayConfig(bootstrapMultiaddr)
if err := os.WriteFile(gatewayPath, []byte(gatewayContent), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write gateway config: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Generated gateway config: %s\n", gatewayPath)
// Print summary
fmt.Printf("\n" + strings.Repeat("=", 60) + "\n")
fmt.Printf("✅ Full network stack initialized successfully!\n")
fmt.Printf(strings.Repeat("=", 60) + "\n\n")
fmt.Printf("Configuration files created in: %s\n\n", debrosDir)
fmt.Printf("Bootstrap Node:\n")
fmt.Printf(" Config: %s\n", bootstrapPath)
fmt.Printf(" Peer ID: %s\n", bootstrapInfo.PeerID.String())
fmt.Printf(" Ports: P2P=4001, HTTP=5001, Raft=7001\n\n")
fmt.Printf("Node2:\n")
fmt.Printf(" Config: %s\n", node2Path)
fmt.Printf(" Ports: P2P=4002, HTTP=5002, Raft=7002\n")
fmt.Printf(" Join: 127.0.0.1:7001\n\n")
fmt.Printf("Node3:\n")
fmt.Printf(" Config: %s\n", node3Path)
fmt.Printf(" Ports: P2P=4003, HTTP=5003, Raft=7003\n")
fmt.Printf(" Join: 127.0.0.1:7001\n\n")
fmt.Printf("Gateway:\n")
fmt.Printf(" Config: %s\n\n", gatewayPath)
fmt.Printf("To start the network:\n")
fmt.Printf(" Terminal 1: ./bin/node --config bootstrap.yaml\n")
fmt.Printf(" Terminal 2: ./bin/node --config node2.yaml\n")
fmt.Printf(" Terminal 3: ./bin/node --config node3.yaml\n")
fmt.Printf(" Terminal 4: ./bin/gateway --config gateway.yaml\n")
fmt.Printf("\n" + strings.Repeat("=", 60) + "\n")
}
func generateNodeConfig(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int, joinAddr, bootstrapPeers string) string {
nodeID := id
if nodeID == "" {
nodeID = fmt.Sprintf("node-%d", time.Now().Unix())
}
// Parse bootstrap peers
var peers []string
if bootstrapPeers != "" {
for _, p := range strings.Split(bootstrapPeers, ",") {
if p = strings.TrimSpace(p); p != "" {
peers = append(peers, p)
}
}
}
// Construct data_dir from name stem (remove .yaml)
dataDir := strings.TrimSuffix(name, ".yaml")
dataDir = filepath.Join(os.ExpandEnv("~"), ".debros", dataDir)
var peersYAML strings.Builder
if len(peers) == 0 {
peersYAML.WriteString(" bootstrap_peers: []")
} else {
peersYAML.WriteString(" bootstrap_peers:\n")
for _, p := range peers {
fmt.Fprintf(&peersYAML, " - \"%s\"\n", p)
}
}
if joinAddr == "" {
joinAddr = "localhost:5001"
}
return fmt.Sprintf(`node:
id: "%s"
type: "node"
listen_addresses:
- "/ip4/0.0.0.0/tcp/%d"
data_dir: "%s"
max_connections: 50
database:
data_dir: "%s/rqlite"
replication_factor: 3
shard_count: 16
max_database_size: 1073741824
backup_interval: "24h"
rqlite_port: %d
rqlite_raft_port: %d
rqlite_join_address: "%s"
discovery:
%s
discovery_interval: "15s"
bootstrap_port: %d
http_adv_address: "127.0.0.1:%d"
raft_adv_address: "127.0.0.1:%d"
node_namespace: "default"
security:
enable_tls: false
logging:
level: "info"
format: "console"
`, nodeID, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, joinAddr, peersYAML.String(), 4001, rqliteHTTPPort, rqliteRaftPort)
}
func generateBootstrapConfig(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int) string {
nodeID := id
if nodeID == "" {
nodeID = "bootstrap"
}
dataDir := filepath.Join(os.ExpandEnv("~"), ".debros", "bootstrap")
return fmt.Sprintf(`node:
id: "%s"
type: "bootstrap"
listen_addresses:
- "/ip4/0.0.0.0/tcp/%d"
data_dir: "%s"
max_connections: 50
database:
data_dir: "%s/rqlite"
replication_factor: 3
shard_count: 16
max_database_size: 1073741824
backup_interval: "24h"
rqlite_port: %d
rqlite_raft_port: %d
rqlite_join_address: ""
discovery:
bootstrap_peers: []
discovery_interval: "15s"
bootstrap_port: %d
http_adv_address: "127.0.0.1:%d"
raft_adv_address: "127.0.0.1:%d"
node_namespace: "default"
security:
enable_tls: false
logging:
level: "info"
format: "console"
`, nodeID, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, 4001, rqliteHTTPPort, rqliteRaftPort)
}
func generateGatewayConfig(bootstrapPeers string) string {
var peers []string
if bootstrapPeers != "" {
for _, p := range strings.Split(bootstrapPeers, ",") {
if p = strings.TrimSpace(p); p != "" {
peers = append(peers, p)
}
}
}
var peersYAML strings.Builder
if len(peers) == 0 {
peersYAML.WriteString("bootstrap_peers: []")
} else {
peersYAML.WriteString("bootstrap_peers:\n")
for _, p := range peers {
fmt.Fprintf(&peersYAML, " - \"%s\"\n", p)
}
}
return fmt.Sprintf(`listen_addr: ":6001"
client_namespace: "default"
rqlite_dsn: ""
%s
`, peersYAML.String())
}
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(" auth <subcommand> 🔐 Authentication management (login, logout, whoami, status)\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(" 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(" config - Show current configuration\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(" Use 'network-cli auth login' to authenticate with your wallet\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 auth login\n")
fmt.Printf(" network-cli auth whoami\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(" ./bin/network-cli pubsub publish notifications \"Hello World\"\n")
}
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 ""
}