readysite / cmd / connect / main.go
3.8 KB
main.go
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"

	"github.com/readysite/readysite/pkg/platform"
)

const usage = `
ReadySite - Connect to Production Servers

Usage:
  connect [flags] [command...]

Flags:
  --server <name>    Connect to specific server by name (auto-selects if only one)

Examples:
  connect                              # SSH (auto-selects single server)
  connect --server app-main-1          # SSH to specific server
  connect docker ps                    # Run command on server
  connect docker logs -f app           # Follow logs
`

// infraConfig defines infrastructure configuration.
type infraConfig struct {
	Servers   map[string]serverSpec `json:"servers"`
	Instances map[string][]instance `json:"instances,omitempty"`
}

type serverSpec struct {
	Size string `json:"size"`
}

// instance represents a deployed server instance.
type instance struct {
	ID     string `json:"id,omitempty"`
	Name   string `json:"name"`
	IP     string `json:"ip,omitempty"`
	Region string `json:"region,omitempty"`
}

// loadInfraConfig loads infra.json from current directory.
func loadInfraConfig() (*infraConfig, error) {
	data, err := os.ReadFile("infra.json")
	if err != nil {
		return nil, fmt.Errorf("read infra.json: %w", err)
	}

	var cfg infraConfig
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("parse infra.json: %w", err)
	}

	return &cfg, nil
}

// allInstances returns all instances across all server types.
func (cfg *infraConfig) allInstances() []instance {
	var all []instance
	for _, instances := range cfg.Instances {
		all = append(all, instances...)
	}
	return all
}

// getInstanceByName finds an instance by name across all server types.
func (cfg *infraConfig) getInstanceByName(name string) *instance {
	for _, instances := range cfg.Instances {
		for i := range instances {
			if instances[i].Name == name {
				return &instances[i]
			}
		}
	}
	return nil
}

// prompt displays a selection menu and returns the selected instance.
func prompt(instances []instance) (*instance, error) {
	fmt.Println("\nMultiple servers found. Select one:")
	fmt.Println()
	for i, inst := range instances {
		fmt.Printf("  [%d] %s (%s)\n", i+1, inst.Name, inst.IP)
	}
	fmt.Println()
	fmt.Print("Enter choice: ")

	var input string
	if _, err := fmt.Scanln(&input); err != nil {
		return nil, fmt.Errorf("read input: %w", err)
	}

	input = strings.TrimSpace(input)
	choice, err := strconv.Atoi(input)
	if err != nil || choice < 1 || choice > len(instances) {
		return nil, fmt.Errorf("invalid selection: %s", input)
	}

	return &instances[choice-1], nil
}

func main() {
	flag.Usage = func() { fmt.Print(usage) }

	serverName := flag.String("server", "", "Connect to specific server by name")
	flag.Parse()

	args := flag.Args()

	// Load infra.json from current directory
	cfg, err := loadInfraConfig()
	if err != nil {
		log.Fatalf("load infra.json: %v", err)
	}

	instances := cfg.allInstances()
	if len(instances) == 0 {
		log.Fatal("no servers found in infra.json (run 'launch' first)")
	}

	// Select target server
	var target *instance
	if *serverName != "" {
		target = cfg.getInstanceByName(*serverName)
		if target == nil {
			log.Fatalf("server %q not found", *serverName)
		}
	} else if len(instances) == 1 {
		target = &instances[0]
	} else {
		target, err = prompt(instances)
		if err != nil {
			log.Fatal(err)
		}
	}

	// Create platform server
	server := &platform.Server{
		ID:   target.ID,
		Name: target.Name,
		IP:   target.IP,
	}

	fmt.Printf("Connecting to %s (%s)...\n", target.Name, target.IP)

	if len(args) > 0 {
		// Run command with connected streams
		if err := server.Connect(args...); err != nil {
			os.Exit(1)
		}
	} else {
		// Interactive SSH session
		if err := server.Interactive(); err != nil {
			log.Fatal("SSH connection failed:", err)
		}
	}
}
← Back