Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/superfly/sprites-go/llms.txt

Use this file to discover all available pages before exploring further.

The Sprites Go SDK handles standard I/O the same way the standard library’s exec.Cmd does: you can attach any io.Reader as stdin and any io.Writer as stdout or stderr, or ask for pipe ends that you read and write yourself. All data is transported over the WebSocket connection transparently.

Attaching readers and writers directly

Set cmd.Stdin, cmd.Stdout, and cmd.Stderr before calling Start or any single-step method (Run, Output, CombinedOutput).
cmd := sprite.Command("grep", "pattern")

// Provide stdin from any io.Reader
cmd.Stdin = strings.NewReader("line 1\nline 2 with pattern\nline 3")

// Capture stdout and stderr into separate buffers
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
    log.Fatal(err)
}

fmt.Println("Stdout:", stdout.String())
fmt.Println("Stderr:", stderr.String())
If Stdin is nil, the remote process reads from /dev/null. If Stdout or Stderr is nil, the corresponding stream is discarded.

Using pipes

Pipes give you an io.WriteCloser for stdin and io.ReadCloser values for stdout and stderr that you can use from your own goroutines. All three pipe methods must be called before Start.

cmd.StdinPipe()

StdinPipe returns a write end you control. Close it when you have finished writing so the remote process sees EOF.

cmd.StdoutPipe()

StdoutPipe returns a read end connected to the process’s stdout stream.

cmd.StderrPipe()

StderrPipe returns a read end connected to the process’s stderr stream.

Full pipe example

The example below streams ten lines to a cat process while reading the output line-by-line with a bufio.Scanner.
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 line by line
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
    fmt.Println("Got:", scanner.Text())
}

// Wait for command to finish
if err := cmd.Wait(); err != nil {
    log.Fatal(err)
}
Always write to StdinPipe in a separate goroutine. Writing in the same goroutine that later calls Wait can deadlock if the pipe buffer fills up before the process exits.

Concurrent I/O patterns

When you need to read stdout and stderr simultaneously, start a goroutine for each pipe so neither blocks the other.
cmd := sprite.Command("some-command")

stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
    log.Fatal(err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
    log.Fatal(err)
}

if err := cmd.Start(); err != nil {
    log.Fatal(err)
}

var wg sync.WaitGroup

wg.Add(1)
go func() {
    defer wg.Done()
    scanner := bufio.NewScanner(stdoutPipe)
    for scanner.Scan() {
        fmt.Println("[stdout]", scanner.Text())
    }
}()

wg.Add(1)
go func() {
    defer wg.Done()
    scanner := bufio.NewScanner(stderrPipe)
    for scanner.Scan() {
        fmt.Println("[stderr]", scanner.Text())
    }
}()

wg.Wait()

if err := cmd.Wait(); err != nil {
    log.Fatal(err)
}
Use bufio.Scanner for line-oriented output and io.Copy when you want to forward the raw byte stream to another writer, such as os.Stdout or an HTTP response body.

Scanner pattern for line-by-line reading

A bufio.Scanner wrapping a StdoutPipe is the idiomatic way to process output one line at a time without buffering the entire output in memory.
cmd := sprite.Command("tail", "-f", "/var/log/app.log")

stdout, err := cmd.StdoutPipe()
if err != nil {
    log.Fatal(err)
}

if err := cmd.Start(); err != nil {
    log.Fatal(err)
}

scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
    line := scanner.Text()
    // Process each log line as it arrives
    fmt.Println(line)
}

if err := cmd.Wait(); err != nil {
    log.Fatal(err)
}

Receiving port notifications

cmd.TextMessageHandler is called for out-of-band text messages that arrive alongside the binary I/O stream. The most common use is reacting to port lifecycle events when the remote process opens or closes a listening port.
import (
    "encoding/json"
    "sync"
)

var (
    proxies = make(map[int]*sprites.ProxySession)
    mu      sync.Mutex
)

cmd := sprite.Command("npm", "start")

cmd.TextMessageHandler = func(data []byte) {
    var notification sprites.PortNotificationMessage
    if err := json.Unmarshal(data, &notification); 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)

        session, err := sprite.ProxyPorts(ctx, []sprites.PortMapping{
            {
                LocalPort:  notification.Port,
                RemotePort: notification.Port,
                RemoteHost: notification.Address,
            },
        })
        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()
    }
}

if err := cmd.Run(); err != nil {
    log.Fatal(err)
}

// Clean up any remaining proxies
mu.Lock()
for port, session := range proxies {
    session.Close()
    delete(proxies, port)
}
mu.Unlock()
TextMessageHandler is called from the WebSocket read loop. Keep the handler fast and non-blocking, or hand off work to a channel or goroutine to avoid stalling I/O delivery.

Choosing the right I/O approach

Assign readers and writers directly when the full input is already available as an io.Reader (for example, strings.NewReader or bytes.NewReader), or when you want to direct output into a buffer or file. This is the simplest approach and requires no goroutine coordination.
Use pipes when you need to produce stdin data or consume stdout/stderr data concurrently with the command running. Pipes are also the right choice when you want to process output incrementally rather than waiting for the process to exit.
Use CombinedOutput when you only care whether the command succeeded and want a single log-friendly byte slice of everything the process printed, regardless of which stream it used.

Build docs developers (and LLMs) love