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 rulesrequire.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.2Remote packages
Hosted in foomo/ownbrew-tap:
ownbrew:
packages:
- name: gotsrpc
tap: foomo/tap/foomo/gotsrpc
version: 2.6.2Tag 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.
