Skip to content

Integrations

Beyond the command interface, posh ships a handful of focused packages you'll reach for repeatedly when authoring a plugin. This page covers the four most important: pkg/exec, pkg/require, ownbrew, and pkg/log.

The exec package

pkg/exec wraps os/exec.Cmd with a middleware chain. Use it instead of calling exec.Cmd.Run() directly when you want cross-cutting concerns like logging, env injection or dry-run.

Direct use

go
import "github.com/foomo/posh/pkg/exec"

err := exec.NewCommand(ctx, "kubectl", "get", "pods").
    Dir(env.ProjectRoot()).
    Env("KUBECONFIG=" + cfgPath).
    Run()

NewCommand defaults to inheriting os.Environ(), with Stdout/Stderr wired to the parent process. The fluent setters mirror exec.Cmd's fields. Run() is a wrapper around exec.Run(ctx, cmd, middlewares...).

Middleware

A middleware is func(next Handler) Handler where Handler = func(ctx context.Context, cmd *exec.Cmd) error. The framework ships a few in pkg/exec/middleware:

MiddlewareWhat it does
middleware.WithEnv(vars...)Appends env vars to cmd.Env
middleware.CaptureStdout(buf)Redirects stdout to a buffer
middleware.CaptureStderr(buf)Redirects stderr to a buffer

Compose them with Middleware(...):

go
var stdout bytes.Buffer

err := exec.NewCommand(ctx, "kubectl", "version", "--client", "-o", "json").
    Middleware(
        middleware.WithEnv("KUBECONFIG=" + cfgPath),
        middleware.CaptureStdout(&stdout),
    ).
    Run()

Middlewares are applied right-to-left around cmd.Run(), so the first one passed is the outermost. Author your own:

go
func WithTimeout(d time.Duration) exec.Middleware {
    return func(next exec.Handler) exec.Handler {
        return func(ctx context.Context, cmd *exec.Cmd) error {
            ctx, cancel := context.WithTimeout(ctx, d)
            defer cancel()
            return next(ctx, cmd)
        }
    }
}

Test commands by replacing the middleware chain with a fake that asserts on cmd.Args and returns canned output.

Why not just pkg/shell?

pkg/shell is for the prompt's fallback: it runs sh -c <line>. That's the right tool when the input is a free-form shell line. For programmatic invocations of specific binaries, pkg/exec is safer — no quoting bugs, no PATH ambiguity, and middleware composes.

Require checks

pkg/require wraps the fender validation library to express preflight checks. The framework already uses it for envs, scripts and packages from .posh.yaml. You can extend it.

Built-ins

go
require.Envs(l, cfg.Envs)         // checks env vars are set
require.Packages(l, cfg.Packages) // host package + version checks
require.Scripts(l, cfg.Scripts)   // run a script; non-zero fails
require.GitUser(l, rules...)      // git user.name / user.email rules

require.GitUser accepts GitUserName and GitUserEmail("regex") rules — useful for enforcing identity conventions in monorepos.

Composing your own

require.First(ctx, l, fends...) accepts mixed types — single Fend, slice of Fend, or fend.Fends. Append your own check:

go
func (p *Plugin) Require(ctx context.Context, cfg config.Require) error {
    return require.First(ctx, p.l,
        require.Envs(p.l, cfg.Envs),
        require.Packages(p.l, cfg.Packages),
        require.Scripts(p.l, cfg.Scripts),
        myDockerCheck(p.l),
    )
}

func myDockerCheck(l log.Logger) fend.Fend {
    return fend.Var("", func(ctx context.Context, _ string) error {
        if err := exec.NewCommand(ctx, "docker", "info").
            Stdout(io.Discard).
            Run(); err != nil {
            return errors.New("Docker daemon is not running. Please start Docker Desktop.")
        }
        return nil
    })
}

require.First short-circuits on the first failure, so order checks from cheapest to most expensive. Use fend.Var(...) to wrap arbitrary functions as fend.Fend values.

You can also surface a require instance as a runtime checker via prompt.WithCheckers(...) — it'll run before the prompt opens and surface its results inline, without exiting the shell.

Ownbrew

foomo/ownbrew is the version-pinned package manager that ships with posh. The default Plugin.Brew implementation just constructs an ownbrew.Brew from .posh.yaml#ownbrew and calls Install.

Local packages

Create a script in .posh/scripts/ownbrew/<name>.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

# Available env vars:
#   $OWNBREW_NAME      package name
#   $OWNBREW_VERSION   requested version
#   $OWNBREW_OS        darwin / linux
#   $OWNBREW_ARCH      amd64 / arm64
#   $OWNBREW_TEMP_DIR  scratch space
#   $OWNBREW_CELLAR_DIR target install dir

curl -L "https://example.com/${OWNBREW_NAME}/${OWNBREW_VERSION}/${OWNBREW_OS}_${OWNBREW_ARCH}.tar.gz" \
  | tar -xz -C "$OWNBREW_CELLAR_DIR"

Reference it from .posh.yaml:

yaml
ownbrew:
  packages:
    - name: my-tool
      version: 1.4.2

Remote packages

Hosted in foomo/ownbrew-tap:

yaml
ownbrew:
  packages:
    - name: gotsrpc
      tap: foomo/tap/foomo/gotsrpc
      version: 2.6.2

Tag filtering

posh brew --tags ci only installs packages whose tag list matches. Use --tags=-ci to exclude. Wire this into your CI image build to skip dev-only tools:

yaml
ownbrew:
  packages:
    - name: gotsrpc
      tap: foomo/tap/foomo/gotsrpc
      version: 2.6.2
      tags: [ci, dev]
    - name: docs-generator
      tap: ...
      version: ...
      tags: [dev]   # skipped on `--tags ci`

Logging

pkg/log defines a single Logger interface used by every framework component and handed to every command constructor. The default implementation prints with pterm colour styling.

Levels

go
l.Trace("very verbose")
l.Debug("debug detail")
l.Info("informational")
l.Success("operation succeeded")  // green check
l.Warn("warning")
l.Error("error")
l.Fatal("error and exit")          // calls os.Exit(1)

The Successf/Warnf/etc. variants take a printf format string. Print/Printf write without a level prefix — use them for command output that the user is supposed to read as data.

Named loggers

go
l := p.l.Named("kube") // -> "[kube] info: …"

Use Named to scope log output for a sub-component. The framework already does this for built-ins (prompt, history, etc.).

slog interop

Logger.SlogHandler() slog.Handler returns a log/slog handler so you can pass the logger to libraries that expect slog. Ownbrew uses this internally.

Test logger

go
import "github.com/foomo/posh/pkg/log"

func TestSomething(t *testing.T) {
    l := log.NewTest(t, log.TestWithLevel(log.LevelDebug))
    // ... pass l to your code under test
}

log.NewTest(t) forwards every log entry to t.Log (and t.Error/t.Fatal for Error/Fatal levels) so failed-test output shows the full sequence inline. Pass log.TestWithLevel(...) to surface lower-level entries.

Putting it together

A real-world command often looks like this — config-driven, exec-wrapped, log-aware:

go
func (c *Deploy) Execute(ctx context.Context, r *readline.Readline) error {
    env := r.Args().At(0)
    target, ok := c.cfg.Environments[env]
    if !ok {
        return fmt.Errorf("unknown environment %q", env)
    }

    c.l.Infof("deploying to %s", env)
    return exec.NewCommand(ctx, "kubectl", "apply", "-f", target.Manifest).
        Middleware(
            middleware.WithEnv("KUBECONFIG=" + target.Kubeconfig),
        ).
        Run()
}

A few lines of business logic; the rest is structural. That's the shape posh is going for.