The Sprite SDK provides an idiomatic Go API for working with sprites. It mirrors the standard exec.Cmd
API to execute commands on remote Sprites as if they were local.
go get github.com/superfly/sprites-go
Note: The import path is github.com/superfly/sprites-go
but the package name is sprites
. You'll need to import it with an alias or the package name will be sprites
.
package main
import (
"fmt"
"log"
sprites "github.com/superfly/sprites-go"
)
func main() {
// Create a client with authentication
client := sprites.New("your-auth-token")
// Get a sprite handle
sprite := client.Sprite("my-sprite")
// Run a command - just like exec.Command!
cmd := sprite.Command("echo", "hello", "world")
output, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Output: %s", output)
}
// Create a client with default settings
client := sprites.New("your-auth-token")
// Or with custom base URL
client := sprites.New("your-auth-token",
sprites.WithBaseURL("http://localhost:8080"))
// Get a sprite handle
sprite := client.Sprite("my-sprite")
The SDK provides a sprite.Cmd
type that works exactly like exec.Cmd
:
// Create a command
cmd := sprite.Command("ls", "-la", "/tmp")
// Run and wait for completion
err := cmd.Run()
// Or get the output
output, err := cmd.Output()
// Or get combined stdout and stderr
combined, err := cmd.CombinedOutput()
cmd := sprite.Command("env")
cmd.Env = []string{"FOO=bar", "BAZ=qux"}
cmd.Dir = "/tmp"
output, err := cmd.Output()
cmd := sprite.Command("grep", "pattern")
// Set stdin from a reader
cmd.Stdin = strings.NewReader("line 1\nline 2 with pattern\nline 3")
// Capture stdout and stderr separately
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
For streaming I/O, use pipes just like with exec.Cmd
:
cmd := sprite.Command("cat")
// Get stdin pipe
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
// Get stdout pipe
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
// Start the command
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
// Write to stdin in a goroutine
go func() {
defer stdin.Close()
for i := 0; i < 10; i++ {
fmt.Fprintf(stdin, "Line %d\n", i)
time.Sleep(100 * time.Millisecond)
}
}()
// Read from stdout
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
fmt.Println("Got:", scanner.Text())
}
// Wait for command to finish
err = cmd.Wait()
Use context for cancellation and timeouts:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := sprite.CommandContext(ctx, "long-running-command")
err := cmd.Run()
// Command will be killed if context times out
Enable TTY mode for interactive commands:
cmd := sprite.Command("bash")
cmd.SetTTY(true)
// Optionally set initial terminal size
err := cmd.SetTTYSize(24, 80)
// Start the command
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
// Resize the terminal while running
err = cmd.Resize(30, 100)
// Wait for completion
err = cmd.Wait()
The SDK provides the same error types as exec.Cmd
:
cmd := sprite.Command("false")
err := cmd.Run()
if err != nil {
// Check if it's an exit error
if exitErr, ok := err.(*sprites.ExitError); ok {
fmt.Printf("Command exited with code: %d\n", exitErr.ExitCode())
} else {
// Other error (connection, auth, etc.)
log.Fatal(err)
}
}
Forward local ports to services running in the sprite:
// Simple port forwarding (same port locally and remotely)
session, err := sprite.ProxyPort(ctx, 3000, 3000)
if err != nil {
log.Fatal(err)
}
defer session.Close()
// Now localhost:3000 connects to the sprite's port 3000
// The session runs until Close() is called or context is cancelled
Forward multiple ports:
sessions, err := sprite.ProxyPorts(ctx, []sprites.PortMapping{
{LocalPort: 3000, RemotePort: 3000},
{LocalPort: 8080, RemotePort: 80},
{LocalPort: 5432, RemotePort: 5432},
})
if err != nil {
log.Fatal(err)
}
defer func() {
for _, s := range sessions {
s.Close()
}
}()
When running commands, you can receive notifications when ports are opened or closed inside the sprite and automatically set up port forwarding:
import (
"encoding/json"
"sync"
)
// Track active proxy sessions
var (
proxies = make(map[int]*sprites.ProxySession)
mu sync.Mutex
)
cmd := sprite.Command("npm", "start")
// Handle port notifications
cmd.TextMessageHandler = func(data []byte) {
var notification sprites.PortNotificationMessage
if err := json.Unmarshal(data, ¬ification); err != nil {
return
}
switch notification.Type {
case "port_opened":
fmt.Printf("Port %d opened on %s (PID %d)\n",
notification.Port, notification.Address, notification.PID)
// Create proxy session with the specific address
session, err := sprite.ProxyPorts(ctx, []sprites.PortMapping{
{
LocalPort: notification.Port,
RemotePort: notification.Port,
RemoteHost: notification.Address, // Use the address from notification
},
})
if err != nil {
log.Printf("Failed to create proxy for port %d: %v", notification.Port, err)
return
}
mu.Lock()
proxies[notification.Port] = session[0]
mu.Unlock()
fmt.Printf("Forwarding localhost:%d -> %s:%d\n",
notification.Port, notification.Address, notification.Port)
case "port_closed":
fmt.Printf("Port %d closed (PID %d)\n", notification.Port, notification.PID)
mu.Lock()
if session, ok := proxies[notification.Port]; ok {
session.Close()
delete(proxies, notification.Port)
fmt.Printf("Stopped forwarding port %d\n", notification.Port)
}
mu.Unlock()
}
}
// Run the command
err := cmd.Run()
// Clean up any remaining proxies
mu.Lock()
for port, session := range proxies {
session.Close()
delete(proxies, port)
}
mu.Unlock()
Here's a complete example showing various features:
package main
import (
"context"
"fmt"
"log"
"strings"
"time"
sprites "github.com/superfly/sprites-go"
)
func main() {
// Create client with authentication
client := sprites.New("your-auth-token",
sprites.WithBaseURL("https://api.sprite.example.com"))
// Get a sprite handle
sprite := client.Sprite("my-sprite")
// Example 1: Simple command with output
output, err := sprite.Command("date").Output()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Current date: %s", output)
// Example 2: Command with pipes and timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := sprite.CommandContext(ctx, "grep", "-i", "error")
cmd.Stdin = strings.NewReader("Line 1\nError on line 2\nLine 3\nAnother ERROR\n")
output, err = cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Grep results:\n%s", output)
// Example 3: Interactive command with environment
cmd = sprite.Command("bash", "-c", "echo Hello $USER from $HOSTNAME")
cmd.Env = []string{"USER=sprite", "HOSTNAME=remote"}
output, err = cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Greeting: %s", output)
}
// Create a new sprites client
client := sprites.New(token string, opts ...Option)
// Available options:
sprites.WithBaseURL(url string) // Set custom API endpoint
sprites.WithHTTPClient(client *http.Client) // Use custom HTTP client
// Get a sprite handle (doesn't create it on the server)
sprite := client.Sprite(name string)
// Create a new sprite (future functionality)
sprite, err := client.Create(name string)
// List sprites (future functionality)
sprites, err := client.List()
The sprite.Cmd
type is designed to be a drop-in replacement for exec.Cmd
. It implements the same methods with the same behavior:
Run()
- Start and wait for completionStart()
- Start the command asynchronouslyWait()
- Wait for a started command to completeOutput()
- Run and return stdoutCombinedOutput()
- Run and return combined stdout/stderrStdinPipe()
- Create a pipe connected to stdinStdoutPipe()
- Create a pipe connected to stdoutStderrPipe()
- Create a pipe connected to stderr
The following fields work identically to exec.Cmd
:
Path
- The command to runArgs
- Command arguments (including Path as Args[0])Env
- Environment variablesDir
- Working directoryStdin
- Standard input (nil, *os.File, or io.Reader)Stdout
- Standard output (nil, *os.File, or io.Writer)Stderr
- Standard error (nil, *os.File, or io.Writer)
The SDK includes comprehensive tests that verify compatibility with exec.Cmd
behavior. Run tests with:
# Tests run only on Linux
go test -v ./sdk/...
See the main project LICENSE file.