Effortless binary manager. Install, update, and organize standalone binaries pulled straight from release pages — no package manager, no build step.
geto downloads release assets from GitHub, GitLab, Codeberg, HashiCorp releases, or go install, picks the right artifact for your OS/arch, unpacks it if needed, and keeps track of what's installed so you can update everything in one command.
A hard fork of marcosnils/bin with a single tagged config, repo descriptions, and a full TUI.
Grab a binary from the releases page, or build from source:
git clone https://github.com/termworks/geto
cd geto
make build # produces ./getomake here is oslo's recipe runner reading
.make.lua; outside an oslo shell it is oslo make. Plain
dub build --build=release works just as well.
Building needs LDC and
dub, plus the OpenSSL, xz, bzip2, zstd and zlib development
headers. The flake's dev shell (nix develop) provides all of them.
Linux only. The D rewrite builds on mochafizz, which targets Linux, so the Windows binaries the Go releases used to ship are no longer produced.
On first run, geto picks a download directory from your PATH (e.g. ~/.local/bin) and creates its config.
geto install github.com/sharkdp/bat # install (alias: add, i)
geto # launch the interactive TUI
geto list # plain table of everything
geto update # update the "default" tier
geto update bat # update a single binary
geto remove bat # uninstall (alias: rm, uninstall, delete)Running geto with no arguments opens the TUI on a real terminal, and falls back to list when piped.
| Command | Aliases | What it does |
|---|---|---|
install <url> [name|path] |
i, add |
Install a binary from a repo/URL |
update [name…] |
u, up, upgrade |
Update binaries (default tier, or named ones) |
ensure [name…] |
e, sync |
Reinstall anything missing or hash-mismatched |
list |
ls, l |
Print a table of managed binaries |
remove <name…> |
rm, uninstall, delete |
Delete the binary and forget it |
prune |
clean, gc |
Forget entries whose files no longer exist |
pin / unpin <name…> |
Freeze / unfreeze a binary's version | |
tag … |
Manage tags/tiers (see below) | |
describe [name…] |
desc |
Fetch & store repository descriptions |
apply <file.json> |
Apply a declarative manifest (see NixOS / Home Manager) | |
ai |
Inspect or reset the learned asset-selection model |
Useful flags on update:
--dry-run— report what would update, change nothing-y, --yes— skip the confirmation prompt-r, --recheck— re-prompt for asset selection instead of reusing the remembered choice-c, --continue-on-error— keep going if one binary fails
Every binary has one or more tags. Untagged binaries belong to default. A persistent --tag/-t flag sets the tag context for any command:
geto install -t essential github.com/junegunn/fzf # install tagged "essential"
geto -t essential update # update only the "essential" tier
geto -t all list # everything, regardless of tag
geto update # == geto -t default update- No
--tag→ acts on thedefaulttier. --tag all→ acts on every binary.
Change tags after the fact:
geto tag ls # list tags and counts
geto tag show bat # show a binary's tags
geto tag add essential bat fzf # add a tag
geto tag rm essential bat # remove a tag (falls back to "default")geto stores each repo's one-line description in the manifest so the TUI can show it offline. New installs fetch it automatically; backfill existing entries with:
geto -t all describe # fetch descriptions for everything missing one
geto describe --force bat # refetch even if already presentFor private/rate-limited repos, export a token first (see Environment).
Run geto (no args) to open the interactive UI: a full-width list whose
entries take three lines each — name and version + update status, then repo,
architecture, libc (musl/glibc/static), size and tags, then the repo
description. The list pages to fit your terminal, with a dot per page at the
bottom and a help line under it.
| Key | Action |
|---|---|
↑/↓, j/k |
move the selection |
←/→, h/l, pgup/pgdn |
previous / next page |
g/G, home/end |
jump to start / end |
/ |
filter by name |
u |
update selected |
r |
check all for updates |
p |
pin / unpin |
e |
edit entry (URL, provider, tags, description) in a popup |
m |
forget the saved asset/archive choice for the selected binary |
o |
open the repository in your browser (xdg-open) |
d / x |
remove (with confirmation) |
t |
cycle the tag scope |
? |
toggle full help |
q |
quit |
On first run geto writes a config file. Colors are terminal palette indexes (0–255) or hex — so pywal-style tools recolor geto automatically, and the 232..255 grayscale ramp gives subtle row shading:
# foreground colors
accent = 1 text = 15 muted = 8
ok = 2 warn = 3 err = 9 tag = 6
# TUI row backgrounds (alternating + selected)
row_bg = 232
row_bg_alt = 235
row_bg_selected = 237| File | Purpose |
|---|---|
$XDG_CONFIG_HOME/geto/list.json |
Manifest — portable: path, url, provider, tags, description |
$XDG_STATE_HOME/geto/config.state.json |
State — per-machine: version, hash, package path, pinned, selected asset, cached description |
$XDG_CONFIG_HOME/geto/config |
TUI colors |
The manifest and per-machine state are kept separate so the manifest is safe to share or check into dotfiles. Config resolution honors $XDG_CONFIG_HOME, falling back to ~/.config/geto (or a legacy ~/.geto).
When run as root without explicit overrides, geto reads /etc/geto/list.json,
writes /var/lib/geto/config.state.json, and installs into /usr/local/bin.
| Provider | Example |
|---|---|
| GitHub | geto install github.com/cli/cli |
| GitLab | geto install gitlab.com/gitlab-org/cli |
| Codeberg | geto install codeberg.org/lukeflo/bibiman |
| HashiCorp | geto install releases.hashicorp.com/terraform |
go install |
geto install goinstall://github.com/x/y |
Asset selection scores candidates by OS/arch and filters out non-installable
files (.sig, .sha256, .sbom, .deb, …), source archives and install
scripts. Assets built for another operating system are dropped, and a release
with no build for your platform fails with a clear message instead of offering
a Windows or macOS artefact — pass --all to pick from everything anyway.
Where a release ships twins, musl wins over glibc/gcc, static over dynamic,
and the plain build over accelerator (-rocm, -mlx) or debug variants.
Distribution packages and source archives never compete with the binary.
When several assets still score identically, geto asks — and remembers the
answer. A small naive-Bayes model over asset-name tokens, plus a nine-feature
network, learns from those answers and resolves clear-cut ties on its own; see
geto ai. Set GETO_NO_AI=1 to turn it off.
Your pick is remembered, so updates don't re-prompt unless the release's file
layout changes (use update -r to force a re-pick).
geto ships a flake package plus NixOS and Home Manager modules. In Nix you
write a normal list of repositories; the module generates list.json, then runs
geto --tag all ensure. ensure downloads missing binaries and fills the
mutable state file with versions, hashes, remembered asset choices, and cached
repository descriptions.
{
inputs.geto.url = "github:termworks/geto";
outputs = { nixpkgs, geto, ... }: {
nixosConfigurations.host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
geto.nixosModules.default
{
programs.geto = {
enable = true;
entries = [
"github.com/rust-lang/mdBook"
{ repo = "github.com/git-town/git-town"; tag = "essential"; }
{ repo = "github.com/atuinsh/atuin"; name = "atuin"; tag = "shell"; }
];
};
}
];
};
};
}For Home Manager:
{
imports = [ inputs.geto.homeManagerModules.default ];
programs.geto = {
enable = true;
entries = [
"github.com/rust-lang/mdBook"
{ repo = "github.com/git-town/git-town"; tag = "essential"; }
];
};
}The generated manifest is just regular geto config:
{
"default_path": "/usr/local/bin",
"bins": {
"/usr/local/bin/git-town": {
"path": "/usr/local/bin/git-town",
"url": "github.com/git-town/git-town",
"tags": ["essential"],
"patch": true
}
}
}For the default root/system paths, create /etc/geto/list.json and run:
sudo geto --tag all ensureYou can also point geto at an explicit manifest/state/install directory:
GETO_CONFIG_FILE=/tmp/geto/list.json \
GETO_STATE_FILE=/tmp/geto/state.json \
GETO_DEFAULT_PATH=/tmp/geto/bin \
GETO_NONINTERACTIVE=1 \
geto --tag all ensureTokens, set as needed:
GITHUB_AUTH_TOKENorGITHUB_TOKEN— GitHub API (avoids the 60 req/hr unauthenticated limit)GITLAB_TOKEN, orGITLAB_TOKEN_<host>for a self-hosted instanceCODEBERG_TOKEN— CodebergGHES_BASE_URL,GHES_UPLOAD_URL,GHES_AUTH_TOKEN— GitHub Enterprise
Paths and behaviour:
| Variable | Effect |
|---|---|
GETO_CONFIG_FILE / GETO_CONFIG_HOME |
where the manifest lives |
GETO_STATE_FILE / GETO_STATE_HOME |
where per-machine state lives |
GETO_DEFAULT_PATH |
install directory |
GETO_NONINTERACTIVE |
fail instead of prompting when a choice is ambiguous |
GETO_NO_AI |
turn off the learned tie-breaker |
The same three paths are also available as --config-file, --state-file and
--default-path.
geto is written in D and built with dub.
Recipes live in .make.lua. At an oslo
prompt in this directory make is enough; everywhere else it is oslo make.
make # the recipes, with what each of them does
make build # the release binary
make run list # run it; bare words pass through, flags go in --args
make test # the suite
make verify # fmt-check + test + build
make static # fully static build (needs musl static libs — see below)
make install # into $PREFIX/bin (default ~/.local/bin)
make release --type minorbuild uses the release build type: -release -O3 -enable-inlining, plus
--function-sections/--data-sections with --gc-sections and -s to drop
unreferenced code and symbols. NATIVE=1 make build adds -mcpu=native, which
tunes for the building machine and is therefore not portable — releases never
use it.
| Path | What lives there |
|---|---|
source/geto/config.d |
manifest + state files, migrations, tag handling |
source/geto/assets.d |
release-asset scoring, filtering and unpacking |
source/geto/archive.d |
tar reader, plus zip and the compression codecs |
source/geto/providers/ |
GitHub, GitLab, Codeberg, HashiCorp, go install |
source/geto/ai/ |
naive Bayes + neural net used to break selection ties |
source/geto/elf.d |
ELF parsing and interpreter/RUNPATH patching |
source/geto/ui/ |
palette, table, progress bar and prompts |
source/geto/cmd/ |
CLI commands and the interactive TUI |
The TUI is built on mochafizz, pinned
by commit in dub.json — which is the only manifest here. The version lives
there too, and source/app.d reads it at compile time, so geto --version
cannot drift from it.
nix build fetches the dub dependencies through a fixed-output derivation, so
moving any dependency means updating outputHash in nix/package.nix — build
once, and Nix prints the hash it wanted.
Release binaries are linked fully static against musl, which needs static
archives for OpenSSL and the compression libraries. Those are only packaged for
musl, so make static and the release workflow run inside Alpine. See
scripts/patch-requests-static.sh for the one upstream gap that has to be
patched first.
MIT — see LICENSE. geto is a hard fork of
marcosnils/bin; see
ACKNOWLEDGMENTS.md.
