| name | adding-tools |
| description | Guide for adding new tools (binaries, linters, formatters) to Pocket. Covers Go packages, GitHub release binaries, Python/uv tools, and Node/bun tools. Use when creating a new tool package under tools/ or modifying an existing tool's installation, versioning, or cross-platform support. |
Adding tools to Pocket
A Pocket "tool" is a reusable binary or package that Pocket downloads, installs,
and makes available. Each tool owns its complete lifecycle: version definition,
installation, and execution.
Directory structure
tools/<toolname>/
├── <toolname>.go # Package with Name, Version, Install task
├── pyproject.toml # (Python tools)
├── uv.lock # (Python tools)
├── package.json # (Node tools)
└── bun.lock # (Node tools)
Required exports
Every tool package must export:
Name constant (the binary name)
Version constant or Version() function
Install variable (*pk.Task, hidden + global)
Version specification
Use inline constants with Renovate comments for automatic updates:
const Version = "1.2.3"
For tools with lockfiles (Python/Node), use the ecosystem's ContentHash:
func Version() string {
return uv.ContentHash(pyprojectTOML, uvLock, []byte(uv.DefaultPythonVersion))
}
func Version() string {
return bun.ContentHash(packageJSON, lockfile)
}
Install task pattern
var Install = &pk.Task{
Name: "install:<tool-name>",
Usage: "install <tool-name>",
Body: installFunc(),
Hidden: true,
Global: true,
}
Installation patterns
Choose the pattern that matches your tool. See PATTERNS.md for
complete examples of each pattern.
Pattern 1: Go package (go install)
For tools written in Go. Simplest pattern.
Body: golang.Install("github.com/org/tool/cmd/tool", Version),
Pattern 2: GitHub release binary
For pre-compiled binaries. The pk/download package provides helper functions
for downloading, extracting archives, and making the binary available in Pocket
(symlink into .pocket/bin/). Use download.Download with platform-aware URLs.
Must handle platform/arch mapping (see cross-platform section in PATTERNS.md).
Pattern 3: Python tool (via uv)
For Python tools. Depends on uv.Install. Uses pyproject.toml/uv.lock with
uv.Sync for reproducible, locked builds. Uses uv.ContentHash for directory
naming and uv.ExecTool for execution (avoids shebang issues). Exposes an
Exec() function instead of a symlink.
Pattern 4: Node tool (via bun)
For Node.js tools. Depends on bun.Install. Embeds package.json and
bun.lock via //go:embed. Uses bun.ContentHash for directory naming.
Exposes an Exec() function instead of a symlink.
Ecosystem tools (uv, bun)
The uv and bun packages are themselves tools (with their own Install
tasks) but also serve as ecosystems for other tools. When your tool depends on
one of these, chain installation with pk.Serial:
Body: pk.Serial(uv.Install, installMyTool()),
Body: pk.Serial(bun.Install, installMyTool()),
These ecosystem tools provide helper functions for creating venvs, installing
packages, and running commands. See PATTERNS.md for details.
uv and Python version coupling
uv bundles Python download metadata at build time. This means the uv version
must be recent enough to know about DefaultPythonVersion. If
DefaultPythonVersion is updated (e.g. by Renovate) to a Python release that
postdates the bundled uv version, uv sync --python <version> will fail on
fresh environments (like CI) with:
error: No interpreter found for Python X.Y.Z in managed installations or search path
It may appear to work locally if that Python version was previously downloaded
by a newer uv. When updating either version, verify the other is compatible. Run
uv python list --only-downloads | grep <version> to confirm the uv binary
knows about the target Python version.
Cross-platform support
All tools must support Linux, macOS, and Windows. Key considerations:
- Use
pk.HostOS() and pk.HostArch() for platform detection
- Use
pk.BinaryName(name) to append .exe on Windows
- Use
pk.DefaultArchiveFormat() ("zip" on Windows, "tar.gz" otherwise)
- Release URL naming varies per project (see PATTERNS.md for mapping examples)
download.WithSymlink() automatically copies instead of symlinking on Windows
- bun on Windows requires extra care (see PATTERNS.md)
Making the tool available
Two approaches depending on tool type:
Symlinked binaries (native/Go tools): Use download.WithSymlink() or
download.CreateSymlink(). The binary is symlinked into .pocket/bin/ which is
on PATH during task execution. Invoke with run.Exec(ctx,Name, args...).
Exec function (Python/Node tools): No symlink. Expose a public
Exec(ctx, args...) function that invokes the tool through its runtime.
Wiring the tool into a task
var Lint = &pk.Task{
Name: "lint",
Usage: "run linter",
Body: pk.Serial(
mytool.Install,
pk.Do(func(ctx context.Context) error {
return run.Exec(ctx,mytool.Name, "run", "./...")
}),
),
}
Idempotency
Always wrap installation logic so it skips if the tool already exists:
download.WithSkipIfExists(binaryPath)
uv.EnsureInstalled(venvDir, name, func(ctx context.Context) error {
})
bun.EnsureInstalled(installDir, name, func(ctx context.Context) error {
})
EnsureInstalled wraps your install function with the correct idempotency
check. Use it instead of manual os.Stat or IsInstalled checks — it makes the
correct pattern impossible to forget.
For Python/uv tools, EnsureInstalled verifies both the tool binary and the
venv's Python interpreter exist. This guards against stale CI caches where
script files remain but the shebang interpreter is missing.
Checklist
- Create
tools/<name>/<name>.go with Name, Version, Install
- Add Renovate comment on version constant/variable
- Handle all three platforms (Linux, macOS, Windows)
- Ensure idempotent installation (skip if exists)
- Set
Hidden: true and Global: true on the Install task
- Wire into a task via
pk.Serial(tool.Install, ...)
- Run
go mod tidy in .pocket/