invoke

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 22, 2026 License: MIT Imports: 11 Imported by: 3

README

invoke

invoke is a Go library for unified command execution. It lets you run commands across Local, SSH, and Docker environments using a single interface.

It solves the "three codepaths" problem: instead of writing separate logic for os/exec, crypto/ssh, and the Docker SDK, you write your logic once and invoke handles the transport.

Why?

Running commands properly is annoying.

  • Local: os/exec is fine, but boilerplate heavy.
  • SSH: crypto/ssh is low-level; you're manually managing sessions and streams.
  • Docker: The SDK is verbose and requires stream demultiplexing.

invoke abstracts this all away.

Installation

go get github.com/ruffel/invoke

Quick Start

1. The Basics

Run a command locally, or on a remote server, just by swapping the provider.

ctx := context.Background()

// env := local.New()
env, _ := ssh.New(ssh.NewConfig("10.0.0.1", "root"))
defer env.Close()

// The Executor wrapper gives you nice helpers (buffering, sudo, etc)
exec := invoke.NewExecutor(env)

// Runs 'uptime' on the remote host
out, _ := exec.RunBuffered(ctx, &invoke.Command{Cmd: "uptime"})
fmt.Println(string(out.Stdout))
2. Streaming Output (The "Real World" Use Case)

For long-running jobs (builds, deploys), you don't want to buffer everything. invoke is streaming-first.

cmd := invoke.Command{
    Cmd: "docker",
    Args: []string{"build", "."},
    Stdout: os.Stdout, // Stream directly to your terminal
    Stderr: os.Stderr,
}

err := env.Run(ctx, cmd)
3. Sudo without the headache

We handle the sudo -n flags and prompt avoidance for you.

// Automatically wraps as: sudo -n -- apt-get update
exec.Run(ctx, cmd, invoke.WithSudo())
4. File Transfer

Uploads and downloads work recursively and consistently across all providers.

// Upload a local directory to a remote server
err := env.Upload(ctx, "./configs/nginx", "/etc/nginx", invoke.WithPermissions(0644))

Design Philosophy

  • Streaming First: We avoid buffering whenever possible. Interfaces use io.Reader and io.Writer.
  • Context Aware: All blocking operations accept context.Context. Cancellation works as you expect (sending signals to remote processes).
  • Secure Defaults: SSH host key checking is enforced by default. You have to opt-out explicitly.

Troubleshooting

"configuration error: HostKeyCheck is missing"

When using the SSH provider, you must verify the server's identity to prevent Man-in-the-Middle attacks.

  • Production: Provide a HostKeyCallback (e.g., parse your ~/.ssh/known_hosts file).
  • Testing: Use c.InsecureSkipVerify = true to disable check (NOT recommended for production).
"failed to dial ssh: ... handshake failed"
  • Check that your SSH key has the correct permissions (chmod 600).
  • Ensure the remote user exists and allows SSH login.
  • Verify AllowUsers or PermitRootLogin in the server's sshd_config.

License

MIT

Documentation

Overview

Package invoke provides a unified interface for command execution and file transfer.

Core Interfaces

- Environment: The connection to a system (Local, SSH, Docker). - Process: A running command handle (allows Wait, Signal, Close).

Streaming

`invoke` is streaming-first. We don't buffer output by default. If you want to capture stdout/stderr, attach an `io.Writer` to your `Command`.

For simple "just give me the output" cases, use the `Executor` wrapper.

Sudo

Privilege escalation is supported via `invoke.WithSudo()`. This uses `sudo -n` for non-interactive execution.

Example (SshConfigReader)
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/ruffel/invoke/providers/ssh"
)

func main() {
	// Example of loading SSH config from a string (or file)
	configContent := `
Host prod-db
  HostName 10.0.0.5
  User admin
  Port 2222
  IdentityFile ~/.ssh/prod_key.pem
  StrictHostKeyChecking no
`
	// Parse the config
	cfg, err := ssh.NewFromSSHConfigReader("prod-db", strings.NewReader(configContent))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Host: %s\n", cfg.Host)
	fmt.Printf("User: %s\n", cfg.User)
	fmt.Printf("Port: %d\n", cfg.Port)

}
Output:
Host: 10.0.0.5
User: admin
Port: 2222

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotSupported indicates that the requested feature (e.g., TTY) is not supported
	// by the specific provider or OS.
	ErrNotSupported = errors.New("operation not supported")
)

Functions

This section is empty.

Types

type BufferedResult

type BufferedResult struct {
	Result
	Stdout []byte
	Stderr []byte
}

BufferedResult extends Result to include captured stdout/stderr content. Returned by Executor.RunBuffered.

type Command

type Command struct {
	Cmd  string   // Binary name or path to executable
	Args []string // Arguments to pass to the binary
	Env  []string // Environment variables in "KEY=VALUE" format
	Dir  string   // Working directory for execution

	// Standard streams. If nil, defaults to empty/discard.
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer

	// Tty allocates a PTY. Useful for interactive commands (e.g. sudo).
	Tty bool
}

Command configures a process execution.

func NewCommand

func NewCommand(binary string, args ...string) *Command

NewCommand creates a new Command with the given binary and arguments.

func ParseCommand

func ParseCommand(cmdStr string) (*Command, error)

ParseCommand parses a shell command string into a Command struct using shlex. It handles quoted arguments correctly.

func (*Command) String

func (c *Command) String() string

String returns a simplified, shell-quoted string representation of the command.

type Environment

type Environment interface {
	io.Closer

	// Run executes a command synchronously.
	// Returns the result (exit code, error). Output is not captured by default; use Command.Stdout/Stderr.
	Run(ctx context.Context, cmd *Command) (*Result, error)

	// Start initiates a command asynchronously.
	// The caller manages the returned Process (Wait/Signal) and must ensure resources are released via Wait().
	Start(ctx context.Context, cmd *Command) (Process, error)

	// TargetOS returns the operating system of the target environment.
	TargetOS() TargetOS

	// Upload copies a local file or directory to the remote destination.
	Upload(ctx context.Context, localPath, remotePath string, opts ...FileOption) error

	// Download copies a remote file or directory to the local destination.
	Download(ctx context.Context, remotePath, localPath string, opts ...FileOption) error
}

Environment abstracts the underlying system where commands are executed (e.g., Local, SSH, Docker).

Example (Upload)
package main

import (
	"context"
	"log"
	"time"

	"github.com/ruffel/invoke"
	"github.com/ruffel/invoke/providers/mock"
)

func main() {
	// Demonstrating the FileTransfer interface usage (now part of Environment)
	var env invoke.Environment = mock.New()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Upload with permissions override
	err := env.Upload(ctx, "./config.json", "/etc/app/config.json",
		invoke.WithPermissions(0o600),
	)
	if err != nil {
		log.Printf("Upload failed: %v", err)
	}
}
Example (Upload_download)
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/ruffel/invoke"
	"github.com/ruffel/invoke/providers/local"
)

func main() {
	var env invoke.Environment = local.New()

	_ = os.WriteFile("localfile.txt", []byte("hello world"), 0o600)
	defer func() { _ = os.Remove("localfile.txt") }()
	defer func() { _ = os.Remove("localfile.bak") }()

	ctx := context.Background()

	err := env.Upload(ctx, "localfile.txt", "/tmp/localfile.txt", invoke.WithPermissions(0o644))
	if err != nil {
		panic(err)
	}

	err = env.Download(ctx, "/tmp/localfile.txt", "localfile.bak")
	if err != nil {
		panic(err)
	}

	content, err := os.ReadFile("localfile.bak")
	if err != nil {
		panic(err)
	}
	fmt.Printf("Downloaded content: %s\n", string(content))

}
Output:
Downloaded content: hello world
Example (WithProgress)
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/ruffel/invoke"
	"github.com/ruffel/invoke/providers/local"
)

func main() {
	var env invoke.Environment = local.New()

	_ = os.WriteFile("largefile.dat", []byte("1234567890"), 0o600)
	defer func() { _ = os.Remove("largefile.dat") }()
	defer func() { _ = os.Remove("largefile.dat.bak") }()

	ctx := context.Background()

	err := env.Upload(ctx, "largefile.dat", "largefile.dat.bak",
		invoke.WithProgress(func(current, total int64) {
			fmt.Printf("Transferred %d/%d bytes\n", current, total)
		}),
	)
	if err != nil {
		panic(err)
	}

}
Output:
Transferred 10/10 bytes

type ExecConfig

type ExecConfig struct {
	Sudo          bool
	RetryAttempts int
	RetryDelay    time.Duration
}

ExecConfig holds configuration derived from options

type ExecOption

type ExecOption func(*ExecConfig)

ExecOption defines a functional option for execution

func WithRetry

func WithRetry(attempts int, delay time.Duration) ExecOption

WithRetry enables retry logic for the command execution using linear backoff. attempts: Total number of attempts (including the initial one). Must be >= 1. delay: Duration to wait between attempts.

func WithSudo

func WithSudo() ExecOption

WithSudo wraps the command in sudo

type Executor

type Executor struct {
	// contains filtered or unexported fields
}

Executor handles command execution with retry logic, sudo support, and output buffering.

func NewExecutor

func NewExecutor(env Environment) *Executor

NewExecutor creates a new Executor with the given environment.

func (*Executor) Download

func (e *Executor) Download(ctx context.Context, remotePath, localPath string, opts ...FileOption) error

Download copies a remote file or directory to the local destination. It delegates directly to the underlying Environment.

func (*Executor) LookPath

func (e *Executor) LookPath(ctx context.Context, file string) (string, error)

LookPath resolves an executable path in the environment, similar to exec.LookPath.

func (*Executor) Run

func (e *Executor) Run(ctx context.Context, cmd *Command, opts ...ExecOption) (*Result, error)

Run executes a command, respecting context cancellation and configured retry policies.

Example (Sudo)
package main

import (
	"context"
	"fmt"

	"github.com/ruffel/invoke"
	"github.com/ruffel/invoke/providers/mock"

	testifymock "github.com/stretchr/testify/mock"
)

func main() {
	// Example of using high-level options
	env := mock.New() // Using mock for safety in example

	// Mock the result
	// Match any command that has the right string components, ignoring pointers (Stdout/Stderr)
	matcher := testifymock.MatchedBy(func(c *invoke.Command) bool {
		return c.Cmd == "sudo" && len(c.Args) == 4 && c.Args[3] == "/root"
	})

	env.On("Run", context.Background(), matcher).Run(func(args testifymock.Arguments) {
		cmd := args.Get(1).(*invoke.Command)
		if cmd.Stdout != nil {
			_, _ = fmt.Fprint(cmd.Stdout, "secret.txt\n")
		}
	}).Return(&invoke.Result{ExitCode: 0}, nil)

	exec := invoke.NewExecutor(env)
	ctx := context.Background()

	// WithSudo() automatically wraps the command
	cmd := invoke.Command{Cmd: "ls", Args: []string{"/root"}}

	// We use RunBuffered to capture output
	res, err := exec.RunBuffered(ctx, &cmd, invoke.WithSudo())
	if err != nil {
		panic(err)
	}

	fmt.Printf("Sudo Output: %s", res.Stdout)
}
Output:
Sudo Output: secret.txt

func (*Executor) RunBuffered

func (e *Executor) RunBuffered(ctx context.Context, cmd *Command, opts ...ExecOption) (*BufferedResult, error)

RunBuffered executes a command and captures both Stdout and Stderr.

Example (Local)
package main

import (
	"context"
	"fmt"

	"github.com/ruffel/invoke"
	"github.com/ruffel/invoke/providers/local"
)

func main() {
	env := local.New()
	defer func() { _ = env.Close() }()

	exec := invoke.NewExecutor(env)

	cmd := invoke.Command{
		Cmd:  "echo",
		Args: []string{"hello", "world"},
	}

	ctx := context.Background()
	res, err := exec.RunBuffered(ctx, &cmd)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s\n", res.Stdout)
}
Output:
hello world

func (*Executor) RunLineStream

func (e *Executor) RunLineStream(ctx context.Context, cmd *Command, onLine func(string), _ ...ExecOption) error

RunLineStream streams stdout line-by-line to onLine. Useful for live logging. Overrides Command.Stdout.

func (*Executor) RunShell

func (e *Executor) RunShell(ctx context.Context, cmdStr string, opts ...ExecOption) (*BufferedResult, error)

RunShell executes a shell command string using the target OS's default shell.

func (*Executor) Start

func (e *Executor) Start(ctx context.Context, cmd *Command) (Process, error)

Start initiates a command asynchronously. Caller is responsible for Process.Wait().

func (*Executor) TargetOS

func (e *Executor) TargetOS() TargetOS

TargetOS returns the operating system of the underlying environment.

func (*Executor) Upload

func (e *Executor) Upload(ctx context.Context, localPath, remotePath string, opts ...FileOption) error

Upload copies a local file or directory to the remote destination. It delegates directly to the underlying Environment.

type ExitError

type ExitError struct {
	Command  *Command
	ExitCode int
	Stderr   []byte // Captured stderr, if available from RunBuffered
	Cause    error  // Underlying error, if any
}

ExitError represents a successful execution that resulted in a non-zero exit code.

func (*ExitError) Error

func (e *ExitError) Error() string

func (*ExitError) Unwrap

func (e *ExitError) Unwrap() error

type FileConfig

type FileConfig struct {
	Permissions os.FileMode // Destination perms override (0 means preserve/default)
	UID, GID    int         // Destination ownership (0 usually means root/current)
	Recursive   bool        // Default true for generic uploads
	Progress    ProgressFunc
}

FileConfig holds configuration for file transfers

func DefaultFileConfig

func DefaultFileConfig() FileConfig

DefaultFileConfig returns defaults

type FileOption

type FileOption func(*FileConfig)

FileOption defines a functional option for file transfers

func WithOwner

func WithOwner(uid, gid int) FileOption

WithOwner forces specific destination ownership

func WithPermissions

func WithPermissions(mode os.FileMode) FileOption

WithPermissions forces specific destination file mode

func WithProgress

func WithProgress(fn ProgressFunc) FileOption

WithProgress calls fn with progress updates

type Process

type Process interface {
	io.Closer

	// Wait blocks until the process exits.
	// Returns an error if the exit code is non-zero.
	Wait() error

	// Result returns metadata (exit code, termination status) (only valid after Wait).
	Result() *Result

	// Signal sends an OS signal to the process.
	// Note: support for specific signals depends on the underlying provider.
	Signal(sig os.Signal) error
}

Process represents a command that has been started but not yet completed.

type ProgressFunc

type ProgressFunc func(current, total int64)

ProgressFunc is a callback for tracking file transfer progress

type Result

type Result struct {
	ExitCode int           // Process exit code (0 indicates success)
	Duration time.Duration // Time taken for execution
	Error    error         // Launch/Transport error (distinct from non-zero exit code)
}

Result contains metadata about a completed command execution.

func (*Result) Failed

func (r *Result) Failed() bool

Failed returns true if the command failed (non-zero exit code or transport error).

func (*Result) Success

func (r *Result) Success() bool

Success returns true if the command completed with exit code 0 and no transport error.

type TargetOS

type TargetOS int

TargetOS identifies the operating system of the target environment.

const (
	// OSUnknown represents an unidentified operating system.
	OSUnknown TargetOS = iota
	// OSLinux represents the Linux kernel.
	OSLinux
	// OSWindows represents Microsoft Windows.
	OSWindows
	// OSDarwin represents macOS (Darwin).
	OSDarwin
)

func DetectLocalOS

func DetectLocalOS() TargetOS

DetectLocalOS returns the TargetOS of the current running process.

func ParseTargetOS

func ParseTargetOS(osStr string) TargetOS

ParseTargetOS converts a typical OS string (e.g., "linux", "darwin") to a TargetOS.

func (TargetOS) ShellCommand

func (os TargetOS) ShellCommand(script string) *Command

ShellCommand constructs a command that runs the provided script inside the system shell. Returns "sh -c <script>" for UNIX-likes and "powershell ..." for Windows.

func (TargetOS) String

func (os TargetOS) String() string

Directories

Path Synopsis
providers
local module
mock module
ssh module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL