zc-workstationDocumentation PDF GitHub

Adding a provider

How to add a provider to zc-workstation, and how to test it without touching the machine.

.md

A provider turns a target into actions. It is one interface:

type Provider interface {
  Plan(ctx context.Context, t config.Target) ([]plan.Action, error)
  Status(ctx context.Context, t config.Target) (string, error)
}

Five optional interfaces feed the dashboard, doctor, list, capture and add:

// Measure counts what is in place, for the progress bars.
type Measurer interface {
  Measure(ctx context.Context, t config.Target) (Live, error)
}

// Programs lists what the provider runs, for the Programs panel.
type Requirer interface {
  Programs(t config.Target) []string
}

// Items lists every declared item, for list and the Inventory page.
type Lister interface {
  Items(ctx context.Context, t config.Target) ([]Item, error)
}

// Capture writes the manifest that matches this machine, for capture.
type Capturer interface {
  Capture(ctx context.Context, t config.Target) ([]byte, error)
}

// Add declares one entry in the manifest, for add. It edits the file only.
type Adder interface {
  Add(t config.Target, e Entry) error
}

A line-based manifest gets sections and whys from internal/manifest: manifest.Notes reads them for Items, and manifest.Insert and manifest.Line write an entry for Add.

Steps

  1. Add a package under internal/provider/. tools is the smallest that drives another program; localdir writes files itself.

  2. Add its name to config.Providers, and its fields and checks to Target.validateFields.

  3. Register it in engine.With.

  4. Deny its import from internal/ui in .golangci.yml, so the page goes through the engine.

  5. Document it in Providers.

Run programs through sh.Runner

A provider that runs other programs takes an sh.Runner, never os/exec directly:

  • Output reads, and its error carries the program’s stderr.

  • Run installs, attached to the command’s streams. Under mcp, those are buffers, never the protocol stream.

  • Has checks PATH.

Tests use sh.Fake. It answers commands from a table and records what Run was given, so a test never touches the machine:

r := &sh.Fake{
  Programs: []string{"mise"},
  Out:      map[string]string{"mise ls --json -C /cfg": `{...}`},
}
actions, err := tools.New(r).Plan(ctx, target)
_ = actions[0].Do(ctx)
// r.Ran[0] == "mise install -C /cfg java@temurin-25"

A test against the real program skips when the program is missing. The dotfiles tests run chezmoi in temporary directories this way.

Rules

  1. Plan writes nothing. Read live state, compare, and return actions. Every write lives in an action’s Do.

  2. Apply does what the plan showed. Act on the planned item only. The dotfiles provider applies one path, not the whole source.

  3. Say what breaks. Detail is what a person reads to decide. Never put a value that could be a secret in it.

  4. MANUAL has no Do. Use it for what no tool should do, and say what a person must do instead.

  5. Writes are verified. The engine plans again after apply. A provider whose write is accepted but not kept shows up as still outstanding; do not hide that by caching.

Edit this page Last updated

Keyboard shortcuts

Ctrl/⌘ K or /
Search the docs
↑ ↓ Enter
Move through the search results
← →
Previous or next page
f
Filter the pages in the sidebar
g h
Home
t
Cycle the theme
?
This list
Esc
Close a menu or the search results