Skip to content

Plugin Overview

A plugin is the Go code your project writes to turn github.com/foomo/posh/pkg/... into a shell tailored to your repo. It's a single Go interface — minus the boilerplate, ~20 lines.

The contract

go
// github.com/foomo/posh/pkg/plugin
type Plugin interface {
    Prompt(ctx context.Context, cfg config.Prompt) error
    Execute(ctx context.Context, args []string) error
    Brew(ctx context.Context, cfg ownbrewconfig.Config, tags []string, dry bool) error
    Require(ctx context.Context, cfg config.Require) error
}

Each method backs one cobra subcommand on bin/posh:

MethodSubcommandCalled by user as
Promptpromptmake shell
Executeexecutebin/posh execute <cmd> [args] (CI, scripts)
Brewbrewbin/posh brew (often make shell.build)
Requirerequirebin/posh require (preflight)

The framework wires each cobra command, parses flags, loads config, and hands you a typed struct. Your job is to wire the implementation — usually just composition of helpers from pkg/....

Optional extensions

Like commands, plugins gain capabilities by also implementing an optional interface. Each is detected by type assertion, so leaving one out is never a breaking change:

InterfaceMethodBacks
CompleterComplete(ctx, args, toComplete) []stringShell completion for posh execute
ListerList(ctx) []CommandInfoposh agent catalog
SkillListerListSkill(ctx) []SkillCommandposh agent skill
SkillMetadataerSkillMetadata(ctx) SkillMetadataThe generated root skill's frontmatter

The scaffolded plugin implements the last three out of the box — see AI Agents for what they produce.

The scaffolded plugin

posh init writes .posh/internal/plugin.go, a near-canonical implementation:

go
type Plugin struct {
    l        log.Logger
    commands command.Commands
}

func New(l log.Logger) (plugin.Plugin, error) {
    inst := &Plugin{
        l:        l,
        commands: command.Commands{},
    }

    inst.commands.Add(command.NewExit(l))
    inst.commands.Add(command.NewHelp(l, inst.commands))

    inst.commands.MustAdd(
        icommand.NewWelcome(l,
            icommand.WelcomeWithConfigKey("welcome"),
        ),
    )

    return inst, nil
}

The constructor:

  1. Builds an empty registry
  2. Registers always-on built-ins (exit, help)
  3. Registers your custom commands

Prompt, Execute, Brew, Require are all implemented in the same file, along with the agent-facing List, ListSkill and SkillMetadata. Read it once, then come back here to extend.

Prompt

go
func (p *Plugin) Prompt(ctx context.Context, cfg config.Prompt) error {
    sh, err := prompt.New(p.l,
        prompt.WithContext(ctx),
        prompt.WithTitle(cfg.Title),
        prompt.WithPrefix(cfg.Prefix),
        prompt.WithAliases(cfg.Aliases),
        prompt.WithCommands(p.commands),
        prompt.WithCheckers(myChecker),
        prompt.WithFileHistory(
            history.FileWithLimit(cfg.History.Limit),
            history.FileWithFilename(cfg.History.Filename),
            history.FileWithLockFilename(cfg.History.LockFilename),
        ),
    )
    if err != nil {
        return err
    }
    return sh.Run()
}

Functional options for everything. Add prompt.WithFlair(...), prompt.WithPrefixGit(true), custom prompt.WithFilter(...) etc. as needed.

Execute

go
func (p *Plugin) Execute(ctx context.Context, args []string) error {
    r, err := readline.New(p.l)
    if err != nil { return err }
    if err := r.Parse(strings.Join(args, " ")); err != nil { return err }

    cmd := p.commands.Get(r.Cmd())
    if cmd == nil {
        return fmt.Errorf("invalid [cmd] argument: %s", r.Cmd())
    }
    if v, ok := cmd.(command.Validator); ok {
        if err := v.Validate(ctx, r); err != nil { return err }
    }
    return cmd.Execute(ctx, r)
}

Parses argv with the same readline parser the prompt uses, then dispatches to the same command instance. Same code path interactively and in CI is the design goal here.

Brew and Require

Both are thin pass-throughs:

go
func (p *Plugin) Brew(ctx context.Context, cfg ownbrewconfig.Config, tags []string, dry bool) error {
    brew, err := ownbrew.New(slog.New(p.l.SlogHandler()),
        ownbrew.WithDry(dry),
        ownbrew.WithBinDir(cfg.BinDir),
        ownbrew.WithTapDir(cfg.TapDir),
        ownbrew.WithTempDir(cfg.TempDir),
        ownbrew.WithCellarDir(cfg.CellarDir),
        ownbrew.WithPackages(cfg.Packages...),
    )
    if err != nil { return err }
    return brew.Install(ctx, tags...)
}

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),
    )
}

You rarely change these. When you do, it's usually to add a custom checker (require.First(ctx, p.l, builtins, myCheck(...))).

What you actually customise

In practice, 90 % of plugin authoring is:

  1. Add commands to the registry in New()
  2. Tweak prompt.With* options to taste
  3. Add custom checkers to Require/Prompt

To let a project recolor its prompt, pass the config key through — the scaffolded plugin already does:

go
prompt.WithTheme(cfg.Theme)

See Theming. Without it the prompt keeps its default colors.

For everything else, see:

  • Writing Commands — the Command interface and its optional siblings
  • Integrationspkg/exec middleware, custom require.Fends, ownbrew taps, logging

A word on dependencies

The .posh/ module pulls in:

  • github.com/foomo/posh (this library)
  • github.com/foomo/ownbrew (transitively, for the brew config types)
  • github.com/spf13/viper (for config decoding in commands)
  • whatever your commands need

Keep it lean. The shell binary is rebuilt every time you change a command — fast builds matter when you're iterating.