Files
posh/docs/plugin/integrations.md
Kevin Franklin Kim a768acb4ca docs: add docs page
2026-05-08 16:38:22 +02:00

7.7 KiB

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

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:

Middleware What 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(...):

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:

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

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:

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:

#!/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:

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

Remote packages

Hosted in foomo/ownbrew-tap:

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:

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

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

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

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:

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.