Packaging & Distribution
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against the documentation of pipx, uv, Homebrew, npm, JBang, GraalVM Native Image, Cargo, the Windows Package
Manager, .NET tools, GoReleaser, Sigstore cosign and GitHub artifact attestations as checked on 2026-09-30, and against the
docsctl running example of this sub-section (fictional, illustrative). The snippets are fragments, not compiled or run here, and every
URL, owner and checksum in them is a placeholder. Names and defaults change between releases, so check the linked page for the
release you use.
A CLI that agents can drive is only useful if it is there: on the developer’s laptop, on the CI runner, inside the sandbox in which the agent runs shell commands. Distribution is therefore part of the tool’s interface. The decisions are which channels to publish to, how a user pins an exact version (an agent that re-resolves "latest" on every run is a reproducibility problem), what the target machine must already have, and how anybody can verify that the file they run is the file you built. The security angle of the last point is treated in Security; the language-specific entry points are in the framework pages (Python, Java, Node.js, Go & Rust and Swift & .NET).
Choosing channels
Publish to the channel that is native to your users' ecosystem first, and add a self-contained binary for everyone else. The table lists the channels of this page; each one has a section below.
| Channel | Install (and pin) | The target machine needs | Notes |
|---|---|---|---|
pipx / |
|
Python (uv can provide one) |
Each tool gets its own isolated environment; the console-script entry point becomes the command. |
Homebrew |
|
Homebrew on macOS or Linux |
A formula in your own tap; users get upgrades with the rest of their packages. |
npm |
|
Node.js |
The |
JBang |
|
A JDK (JBang can download one) |
A single source file with |
GraalVM native binary |
Download, verify, put on the |
Nothing (one build per OS and architecture) |
Fast start-up, the best default for agents; the build needs the native toolchain of each platform. |
|
|
A Rust toolchain |
Compiles from source, so it is slow; publish prebuilt binaries as well. |
winget |
|
Windows with the Windows Package Manager |
A manifest submitted to the community repository. |
|
|
The .NET SDK |
A NuGet package; a local tool manifest pins the version per repository. |
GoReleaser |
Not a channel: builds the archives, checksums and signatures above |
n/a |
One |
Container image |
|
A container runtime |
A reproducible runtime for CI and sandboxes; pin by digest. |
For Rust tools, cargo install and the [package] metadata that crates.io needs are covered in the Rust Reference’s
Cargo & Workspaces.
The flow from a tag to the channels is the same for every ecosystem: build once per platform, checksum and sign the outputs, publish them, then let each package manager point at the published files.
pipx and uv tool for Python
pipx installs and runs end-user Python applications in isolated environments: it creates a separate virtual environment for each
application and exposes only its commands on the PATH, so a tool’s dependencies never collide with a project’s. pipx install installs a
tool, pipx run executes the latest version temporarily without installing it, pipx upgrade updates it and pipx ensurepath fixes the
PATH. uv offers the same model as uv tool: uv tool install installs, uvx is an alias for uv tool run, uv tool upgrade updates, and
uv tool update-shell adds the executable directory to the PATH. Both work for any package that declares console-script entry points, which
is what [project.scripts] does in the Python framework page.
pipx install docsctl # isolated environment, docsctl on the PATH
uv tool install docsctl@0.1.0 # pinned to an exact version
uvx docsctl@0.1.0 search kalman --json # run once, without installing
Pin the version in anything an agent or a CI job runs (docsctl@0.1.0), so the tool cannot change under a working prompt. References:
pipx and uv — Tools.
Homebrew formulae
A Homebrew formula is a small Ruby file with a homepage, a license, the url and sha256 of a source or binary archive, an install
method and a test do block. Formulae for your own tool live in a tap, a Git repository named homebrew-<name> under your account, so
users install with brew install OWNER/tap/docsctl after a one-time brew tap. The Formula Cookbook asks for tests that need no user input
and exercise real functionality rather than only --version or --help, and recommends brew audit --new --formula before submission.
class Docsctl < Formula
desc "Search and publish this site's documentation"
homepage "https://example.invalid/docsctl"
url "https://example.invalid/releases/docsctl-0.1.0-macos-arm64.tar.gz"
sha256 "<sha256 of the archive, copied from the checksums file>"
license "Apache-2.0"
def install
bin.install "docsctl"
end
test do
# A read-only command with no network access and no prompts
assert_match "\"schemaVersion\"", shell_output("#{bin}/docsctl schema --json")
end
end
The test block above uses the contract’s schema --json, which is read-only and never prompts, so it passes in the sandboxed test run.
GoReleaser can generate and push the tap update on every release; its documentation lists Homebrew formula publishing as deprecated in
favour of homebrew_casks, so check which one your GoReleaser version documents before adopting either. References:
Homebrew — Formula Cookbook.
npm bin
An npm package exposes commands through the bin field of package.json, either as a string (one command named after the package) or as
an object mapping command names to files. The referenced file must start with #!/usr/bin/env node, otherwise it is not started with
Node. A global install links the file into a directory on the PATH (a .cmd shim on Windows); a local install makes it available in
node_modules/.bin, from where npx can run it.
{
"name": "docsctl",
"version": "0.1.0",
"bin": { "docsctl": "bin/docsctl.js" },
"files": ["bin", "dist"]
}
#!/usr/bin/env node
import "../dist/main.js";
Keep the entry file tiny so that start-up cost stays low, as described in Agent Ergonomics, and use
npx docsctl@0.1.0 … (an exact version) when an agent runs the tool without installing it. Reference:
npm — package.json bin.
JBang
JBang runs a single Java source file as a script: //DEPS comment lines declare Maven dependencies, and a #!/usr/bin/env jbang shebang line makes
the file executable (see the JBang documentation for how to write it in a Java file). It needs a JDK and downloads one when none is available. jbang app install installs a script as a command, and jbang app
list shows what is installed. JBang can also run a file from a URL or a Maven coordinate, and a catalog lets users run a published tool as
jbang docsctl@OWNER; because that runs remote code, jbang trust exists to make the user authorise the source first.
DocsctlCommand is the command class from that page)//DEPS info.picocli:picocli:4.7.7
public class Docsctl {
public static void main(String... args) {
System.exit(new picocli.CommandLine(new DocsctlCommand()).execute(args));
}
}
jbang app install Docsctl.java # puts a docsctl launcher on the PATH
jbang app list
JBang is a good way to hand a JVM tool to Java developers without a release pipeline; it does not remove the JVM start-up cost, which is what the next section addresses. Reference: JBang documentation.
GraalVM native binaries
GraalVM Native Image compiles a Java application ahead of time into a standalone executable. The documentation describes executables that use
fewer resources than the JVM, start quickly and reach peak performance without warm-up, which is why native binaries are the usual default for a
tool an agent calls many times; measure with the hyperfine commands on the Java page before
choosing. Two constraints shape packaging. First, the closed-world assumption: the build analyses which code is reachable, so reflection,
resources and dynamic proxies need metadata (picocli-codegen produces it for picocli commands, see the Java page). Second, the build needs the
platform’s native toolchain, so you build on each target operating system and architecture (a CI matrix) rather than cross-compiling one
artifact for all of them.
native-image -jar target/docsctl-0.1.0.jar docsctl
./docsctl --version
Run the contract tests of Testing CLIs against the native binary, not only against the JAR, because a missing metadata entry appears only in the native build. Publish one archive per platform and list its checksum. Reference: GraalVM Native Image.
cargo install
cargo install compiles a crate that has a binary target and puts the executable in <root>/bin (by default $HOME/.cargo/bin, chosen with
--root, CARGO_INSTALL_ROOT, install.root or CARGO_HOME). --locked uses the dependency versions from Cargo.lock, so the user builds what you tested;
--path installs from a checkout, --git from a repository, and --force reinstalls over an existing binary.
cargo install docsctl --locked # from the registry (placeholder crate name)
cargo install --path . --locked # from a checkout
cargo install --list # what is installed
cargo uninstall docsctl
Because it compiles on the user’s machine it needs a Rust toolchain and takes a while, which suits Rust developers and not CI runners or agent
sandboxes. Publish prebuilt archives too, and let cargo install be the fallback. Reference:
cargo install.
winget
The Windows Package Manager installs from manifests: YAML files that follow the manifest schema and live in the community repository
(microsoft/winget-pkgs). A manifest names the PackageIdentifier (the Publisher.Package form), PackageVersion, publisher, licence and
description, and one or more installers with an architecture, an installer type, the InstallerUrl and its InstallerSha256. The
wingetcreate utility (wingetcreate new) prompts for the values, and can submit the pull request for you.
PackageIdentifier: Publisher.Docsctl
PackageVersion: 0.1.0
PackageLocale: en-US
Publisher: Publisher
PackageName: docsctl
License: Apache-2.0
ShortDescription: Search and publish this site's documentation.
Installers:
- Architecture: x64
InstallerType: <a supported installer type>
InstallerUrl: https://example.invalid/releases/docsctl-0.1.0-windows-amd64.zip
InstallerSha256: <sha256 of the file at InstallerUrl>
ManifestType: singleton
ManifestVersion: 1.12.0
The single-file form is valid only for one installer and one locale; use the multi-file form otherwise. winget accepts installers only in its
list of supported formats and requires silent installation, so check the current list on the winget documentation and choose the installer type
for a command-line tool from it; a plain executable is rarely the right shape for one. GoReleaser has a winget section that generates the
manifest and opens the pull request from your release. References:
Windows Package Manager — submit packages and
Create your package manifest.
dotnet tool
A .NET tool is a NuGet package containing a console application. Two project properties, PackAsTool and ToolCommandName, turn a project into one,
dotnet pack builds the package and dotnet nuget push publishes it. Users install it globally (dotnet tool install --global), in a custom
location (--tool-path) or as a local tool recorded in .config/dotnet-tools.json, which dotnet tool restore reinstalls, pinning the version per repository. The
project fragment and the three install forms are on the Swift & .NET page; the
Microsoft documentation warns that tools run in full trust and that the manifest decides what dotnet tool run executes, so keep it under review.
dotnet pack -c Release
dotnet nuget push ./nupkg/docsctl.0.1.0.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json
References: .NET tools and Create a .NET tool.
GoReleaser
GoReleaser automates the release: it builds the binaries, archives them, writes checksums, signs, and can publish to package managers
(Homebrew and winget among them), Docker images and SBOM generation. Its configuration is .goreleaser.yaml; goreleaser init creates one,
goreleaser check validates it, goreleaser release --snapshot --clean produces a local trial release without publishing, and
goreleaser release performs the real one. It documents Go, Rust, TypeScript, Zig and Python projects, so it also fits a Rust CLI such as the
docsctl Rust variant.
version: 2
builds:
- main: ./cmd/docsctl
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
archives:
- formats: ["tar.gz"]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
format_overrides:
- goos: windows
formats: ["zip"]
checksum:
name_template: checksums.txt
signs:
- cmd: cosign
signature: "${artifact}.sigstore.json"
args: ["sign-blob", "--bundle=${signature}", "${artifact}", "--yes"]
artifacts: checksum
Always run the snapshot form in CI on pull requests, so a broken release configuration fails before the tag rather than on it. References: GoReleaser, archives and signing.
Container images
A container image gives CI jobs and sandboxes an identical runtime without installing anything on the host. The general practices (minimal, pinned
base image, multi-stage builds, a non-root user, no secrets baked in) are documented on the
Docker reference, in particular
Image Best Practices and Supply-Chain Security; the image for a native docsctl
follows them.
FROM debian:stable-slim
COPY docsctl /usr/local/bin/docsctl
USER 65532:65532
ENTRYPOINT ["docsctl"]
The ENTRYPOINT in exec form makes the image behave like the CLI: docker run --rm IMAGE search kalman --json prints JSON on stdout and returns
docsctl’s exit code as the container’s. Three habits matter for agents. Do not allocate a terminal (-t) when the output is parsed, because a
TTY changes the tool’s behaviour as described in TTY, Colour & Interactivity. Pass credentials
by forwarding the variable (-e DOCSCTL_TOKEN with no value copies it from the host environment) or by mounting a token file read-only, and never
COPY a credential into an image. And pin the image by digest, not by a tag that can move.
Signing and checksums
A release should carry evidence of what it is.
-
Checksums. Publish a file of SHA-256 checksums with every release (GoReleaser’s
checksumsection writes it). Package manager manifests (the Homebrewsha256, the wingetInstallerSha256) repeat those values, so the file is also the source of truth for them. A checksum proves the download is intact; it proves who built it only if the checksum file is itself signed or served over a trusted path. -
Signatures. Sigstore’s
cosign sign-blobsigns a file; the documentation recommends--bundleso that the signature, certificate and transparency-log proof travel together, and describes keyless signing, which ties the signature to an OpenID Connect identity, and--yesfor unattended CI runs. -
Build attestations. GitHub’s
actions/attestaction records build provenance for a workflow’s output (the job needs theid-token: write,contents: readandattestations: writepermissions), andgh attestation verify PATH -R OWNER/REPOverifies it.
sha256sum --check checksums.txt --ignore-missing
gh attestation verify docsctl_0.1.0_linux_amd64.tar.gz -R OWNER/docsctl
The full verification recipe with cosign verify-blob, the identity and issuer to accept, and the caveat that an attestation links an artifact
to its source and build rather than certifying it as safe, is in the "Signed releases" section of
Security. References:
Sigstore — Signing blobs and
GitHub — Use artifact attestations.
Checklist
-
One tag produces every channel, from one CI workflow, and a snapshot build runs on pull requests.
-
Every channel can install an exact version, and the documentation (and any skill) shows the pinned form.
-
The binary needs nothing at run time that the channel does not provide (Python, Node, a JDK) or is native.
-
docsctl --versionprints the same version in every channel, anddocsctl schema --jsonreports it too. -
Checksums are published and signed, and manifests reference them.
-
The contract tests of Testing CLIs pass against the packaged artifact.
-
Man pages and completions are in the package (see Documenting CLIs).
-
The whole list is condensed in the Agent-Readiness Checklist.