A command cache: governor runs a command, caches its stdout/stderr/exit
code for a TTL, and replays that result on repeat invocations instead of
running the command again. Useful for anything slow, rate-limited, or just
wasteful to re-run on every single invocation — a self-update check, a
shell prompt segment, a network call you don't want to repeat on every
script run.
Wrap a frequently self-updating CLI so the update check only actually happens once every few hours instead of on every invocation:
thatcmd() { governor run --ttl 4h --silent -- brew upgrade thatcmd; command thatcmd "$@"; }--silent suppresses the upgrade check's own output (you don't want to see
"Already up-to-date" on every invocation), and the real thatcmd "$@" call
after it is what actually runs and receives your real arguments — governor
only ever wraps and throttles the upgrade check, never the tool invocation
itself.
Prompt segments that shell out over the network (current AWS account, cloud project, etc.) add real, noticeable latency to every single prompt render. Cache the check with a short TTL so the prompt stays fast and only refreshes periodically:
governor run --ttl 30s --silent -- aws sts get-caller-identity --query Account --output text$ governor run --ttl 1h -- curl -s https://api.ipify.org
203.0.113.42
$ governor run --ttl 1h -- curl -s https://api.ipify.org # cached, no network round trip
203.0.113.42Handy in scripts you run repeatedly during development, or against APIs with rate limits you don't want to burn through on every run.
brew install calini/tap/governorOr, with Go 1.25+:
go install github.com/calini/governor/cmd/governor@latestgovernor run --ttl 4h -- some-command --with --args
governor peek -- some-command --with --args # replay the last result, stale or not
governor forget -- some-command --with --args # evict the cached entry
governor list # show what's cached, and whether it's still fresh
governor purge # sweep the whole store for expired entries
governor --help # overview of all commands
governor run --help # flags for a specific commandBy default the cache key is derived from the wrapped command's argv. Use
--vary-cwd, --vary-path, --vary-env=VAR (repeatable), or
--vary-modtime=FILE (repeatable) to fold additional inputs into the key —
named after the HTTP Vary header, which does the same thing for response
caching. A changed input simply produces a different key; there's no
separate invalidation step. --key overrides the derived key entirely
(bypassing all of the above as key-folding inputs — --scope still tags
the stored record and remains filterable via governor list --scope /
governor purge --scope even when --key is set).
--scope STRING takes on two related roles depending on the subcommand.
On run/peek/forget it's a key-folding input, same as a --vary-*
flag: it folds an arbitrary string into the cache key and tags the stored
entry with it. On list/purge it's instead a filter over already-stored
entries: governor list --scope / governor purge --scope narrow to just
the entries tagged with that scope, letting you namespace entries you want
to inspect or sweep together later. What a scope represents (an
environment, a project, a tenant) is entirely up to you; governor attaches
no meaning to the string itself. Entries with different scopes never
collide, and omitting --scope behaves exactly as before — fully
additive, opt-in.
governor run --ttl 1h --scope prod -- curl -s https://api.prod.example.com/status
governor list --scope prod # only prod-scoped entries
governor purge --scope prod # only sweep expired prod-scoped entries--ttl DURATION(required forrun) — accepts stdlibtime.ParseDurationunits plusd(day) andw(week), e.g.4h,1d,2w,1d12h.--force— skip the freshness check and execute unconditionally.--scope STRING— fold a scope into the cache key and tag the entry with it; see Scopes.--discard-failures— on non-zero exit, leave the cache untouched instead of caching the failure (the default caches failures too, overwriting any previously cached success). A prior good result keeps being served until the command succeeds again; if there wasn't one, it just stays a miss.--silent— capture output without streaming it to the terminal.--cache-dir DIR— override the cache directory (also settable via theGOVERNOR_CACHE_DIRenv var; defaultos.UserCacheDir()/governor).
governor completion <shell> generates a completion script for bash, zsh,
fish, or PowerShell — e.g. source <(governor completion zsh). Run
governor completion --help or governor completion <shell> --help for
shell-specific setup instructions.
governor runand agovernor peekcache-hit both pass through the wrapped/cached command's own exit code — including when the command ran successfully but the cache write afterward failed; a caching problem never overrides the command's real exit code.3is used for internal governor errors that happen before the command ever runs (bad flags, cache open failure, etc.). This can collide with a wrapped command that itself legitimately exits3— a known, accepted limitation of exit-code passthrough, not something governor tries to solve.governor peek/governor forgetexit1when nothing is cached for the derived key / nothing was found to forget.- A command killed by a signal is cached with exit code
-1(Go'sos/execconvention for signal death), which the OS then reports as exit255when governor itself exits with it.
import "github.com/calini/governor"
cmd := governor.Command(exec.Command("some-command"))
result, err := cmd.Run(ctx, 4*time.Hour)governor.Command wraps an *exec.Cmd, mirroring exec.Command's own naming — every exec.Cmd field and method (Dir, Env, Stdout, Start, Wait, ...) is available directly on it. Set Key/Scope/VaryCwd/VaryPath/VaryEnv/VaryModtime/Force/DiscardFailures/CacheDir before calling Run, Peek, or Forget. List(cacheDir, scope string) ([]ListEntry, error) and Purge(cacheDir, scope string) (int, error) operate on the whole store rather than a single Cmd. See the package docs for details.
See docs/design.md.