Poly

One command, every package manager. Installs from pip, npm, and verified binary releases on macOS, Linux, and Windows.(server at https://iuymslcbbrbahxbfuzrr.supabase.co/functions/v1/mcp)

Documentation

poly

English · Français

One command, every package manager. poly installs from pip, npm, Homebrew, and checksum-verified binary releases behind a single unified command, on macOS, Linux, and Windows.

Site + account management: poly.candygate.eu

$ poly install ripgrep
downloading ripgrep  [############################] 100%  1.7MiB/1.7MiB
installed ripgrep 15.1.0 (via tap)
note: tap binaries are installed to ~/.poly/bin — make sure it's on your PATH

$ poly install requests@2.31.0
installed requests 2.31.0 (via pip)

$ poly install npm:cowsay
installed cowsay 1.6.0 (via npm)

$ poly install brew:jq
installed jq 1.8.2 (via brew)

$ poly list
NAME      VERSION  ADAPTER  INSTALLED
cowsay    1.6.0    npm      2026-07-11 15:52
jq        1.8.2    brew     2026-07-11 17:30
requests  2.31.0   pip      2026-07-11 15:52
ripgrep   15.1.0   tap      2026-07-11 15:52

Install

macOS — download poly-macos.pkg, open it, click through the installer. Installs a universal (Intel + Apple Silicon) binary to /usr/local/bin/poly. It's unsigned/unnotarized (no Apple Developer account yet), so Gatekeeper will warn on first open — right-click → Open, or System Settings → Privacy & Security → Open Anyway.

Windows — download poly-setup.exe, run it. Installs to %LOCALAPPDATA%\Poly and adds it to your user PATH (no admin rights needed). It's unsigned, so SmartScreen will warn — "More info" → "Run anyway". (Built cross-platform via NSIS; not yet verified on a real Windows machine — please report issues.)

From source (any OS, needs Go 1.21+):

git clone https://github.com/opencorp2030-ctrl/poly.git
cd poly
go build -o poly .
./poly version           # sanity check: runs the binary in place

# put it on PATH, e.g.:
cp poly /opt/homebrew/bin/poly   # macOS (Homebrew's bin, no sudo needed)
# or: sudo cp poly /usr/local/bin/poly       # macOS/Linux, needs sudo
# or: add this folder to PATH yourself

poly version              # now works from anywhere

Rebuild the installers yourself with VERSION=x.y.z installers/build.sh (needs makensis for the Windows one: brew install makensis).

Cross-compile for another OS/arch with GOOS/GOARCH, or run scripts/build-all.sh to build all five targets (darwin/linux/windows × amd64/arm64) into dist/.

🖥️ Poly Desktop — now available for Windows

Poly Desktop is a native desktop app for managing your Poly account without a browser: publish and edit apps with a live store-listing preview, search/install/remove packages through the real CLI, browse the community and the public apps store, real notifications with native OS alerts, and a full theme/accent color picker.

⬇ Download Poly Desktop for Windows — run the installer, no admin rights needed. Signs in with the same account as poly login on the CLI.

macOS and Linux builds are coming very soon. Windows ships first because that's what's been tested end-to-end; the app itself is plain Electron/Node, so the other platforms are mostly a packaging step away. Star/watch the repo to know when they land.

How package resolution works

poly install <name> tries each adapter's Search in order — tap → brew → pip → npm → cargo → go — and installs through the first one that reports the package exists. Force a specific backend with a prefix:

CommandResolves to
poly install ripgrepfirst match: tap → brew → pip → npm → cargo → go
poly install tap:ripgrepforced binary download from the tap formula
poly install brew:wgetforced brew install wget
poly install pip:requestsforced pip install requests
poly install npm:lodashforced npm install -g lodash
poly install cargo:ripgrepforced cargo install ripgrep
poly install go:golang.org/x/tools/cmd/goimportsforced go install <module>@latest
poly install gem:railsforced gem install rails
poly install apt:ripgrepforced apt-get install ripgrep (Debian/Ubuntu)
poly install dnf:ripgrepforced dnf install ripgrep (Fedora/RHEL)
poly install pacman:ripgrepforced pacman -S ripgrep (Arch)
poly install winget:BurntSushi.ripgrepforced winget install --exact (Windows)
poly install choco:ripgrepforced choco install ripgrep (Windows)

gem, apt, dnf, pacman, winget, and choco are explicit-prefix only, like community and gh below — never auto-resolved, so a system package or a single-language gem can't silently shadow the tap/brew/pip/npm/cargo/go chain.

Append @version to pin: poly install requests@2.31.0. pip, npm, and cargo pass that straight through; the tap adapter only offers the version pinned in its formula and errors if you ask for a different one; go treats it as the module's version/tag.

Commands

CommandDoes
poly install [[adapter:]name[@version] ...]install packages, auto-routed or forced; no args installs from poly.json
poly remove nameuninstall, via whichever adapter installed it
poly listshow everything poly has installed, and through what
poly search [adapter:]namecheck existence and latest version across adapters
poly info [adapter:]nameversion, summary, homepage, and install status, per adapter
poly initwrite poly.json from your currently installed packages
poly upgradeupdate every installed package to its adapter's latest version
poly self-updateupdate the poly binary itself to the latest GitHub release
poly accountshow your signed-in email/username/bio/plan
poly login / poly logoutsign in/out of your Poly account (unlocks Pro)
poly versionprint the poly build version and, if signed in, your account/plan

State lives in ~/.poly/manifest.json. Tap binaries land in ~/.poly/bin (add it to your PATH). Login credentials live in ~/.poly/credentials.json (mode 0600). poly self-update and, for Pro accounts, poly upgrade also run automatically in the background (throttled to ~once/day, logged to ~/.poly/auto-update.log) so you don't have to remember to run them.

Reproducible installs with poly.json

poly init writes a poly.json in the current directory listing every package poly has installed, pinned to its exact version:

{
  "packages": [
    "tap:ripgrep@15.1.0",
    "npm:eslint@9.2.0",
    "pip:requests@2.31.0"
  ]
}

Commit it, and anyone (or any CI machine) can reproduce the same set with a plain poly install — no arguments needed.

Poly Pro

Poly is and stays 100% free and open source — the whole built-in tap catalog (ripgrep, fd, jq, ...) is free too, on purpose: Pro isn't about withholding tools people expect for free. The one real, measurable perk today:

  • poly install a b c (multiple packages in one command) installs them sequentially on the free tier; signed in with an active Pro plan (poly login), the same command installs them concurrently, and poly upgrade runs automatically in the background instead of only on demand.

Manage your account (sign up, check your plan) at poly.candygate.eu/account.html, backed by Supabase Auth — see internal/account/account.go for the client-side logic and the public.profiles table for where plan status lives.

Adapters

  • pip — shells out to pip3/pip, streaming its output live so you see pip's own download progress. Search hits the PyPI JSON API (pypi.org/pypi/<name>/json), an exact-name lookup — PyPI has no public free-text search API anymore.
  • npm — shells out to npm install -g, same live-streamed output. Search hits registry.npmjs.org/<name>/latest.
  • brew — shells out to a local brew install/brew uninstall, same live-streamed output. Search hits the public formulae.brew.sh/api/formula/<name>.json, so it works even without brew installed (only actually installing needs it).
  • cargo — shells out to cargo install --force/cargo uninstall. Search hits the public crates.io/api/v1/crates/<name> API.
  • go — shells out to go install <module>@version. Packages here are full module import paths, not short names (e.g. golang.org/x/tools/cmd/goimports), so this one is realistically only used via an explicit go: prefix. Search walks the path upward against the public Go module proxy until it finds the enclosing module, the same resolution go install does internally.
  • tap — installs prebuilt binaries directly from a pinned URL, with a live byte-progress bar during download, verified against a SHA-256 checksum, then extracted (.tar.gz/.zip) or copied into ~/.poly/bin. No Python, Node, Rust, or Homebrew needed.
  • gem — shells out to a local gem install/gem uninstall. Search hits the public rubygems.org/api/v1/gems/<name>.json API. Explicit gem: prefix only, like cargo/go.
  • apt — shells out to a local apt-get install/apt-get remove on Debian/Ubuntu. Search reads local apt-cache show output, no network call needed. Explicit apt: prefix only — auto-resolving a bare name to a system package would be a surprise on Linux.
  • dnf — same idea on Fedora/RHEL, via dnf install/dnf remove and dnf info. Explicit dnf: prefix only.
  • pacman — same idea on Arch, via pacman -S/pacman -R and pacman -Si against the synced repos (not the AUR). Explicit pacman: prefix only; pacman has no built-in way to pin an arbitrary version from the regular repos, so version pins are rejected.
  • winget — shells out to a local winget install --exact/winget uninstall on Windows. Search reads local winget show --exact output. Explicit winget: prefix only.
  • choco — shells out to a local choco install/choco uninstall (Chocolatey) on Windows. Search reads local choco search --exact --limit-output output. Explicit choco: prefix only.

Adding a tap formula

Drop a YAML file into ~/.poly/taps/<name>.yaml (this overrides any built-in formula of the same name — see internal/registry/embedded/taps for the ones that ship with poly):

name: ripgrep
description: "Line-oriented search tool that recursively searches directories for a regex pattern"
homepage: "https://github.com/BurntSushi/ripgrep"
version: "15.1.0"
binary: rg
tier: free   # omit for free, or "pro" to gate it behind an active Pro plan
artifacts:
  darwin_arm64:
    url: "https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-aarch64-apple-darwin.tar.gz"
    sha256: "378e973289176ca0c6054054ee7f631a065874a352bf43f0fa60ef079b6ba715"
  darwin_amd64: { url: "...", sha256: "..." }
  linux_amd64: { url: "...", sha256: "..." }
  linux_arm64: { url: "...", sha256: "..." }
  windows_amd64: { url: "...", sha256: "..." }

The artifact key is <GOOS>_<GOARCH>. poly install <name> picks the entry matching the machine it's running on. Built-in formulas today, all free: ripgrep, fd, jq — see internal/registry/embedded/taps.

Project layout

main.go                          entrypoint
cmd/                              cobra commands (install, upgrade, init, info, account, self-update, ...)
internal/manifest/                ~/.poly/manifest.json read/write
internal/lockfile/                poly.json read/write
internal/adapters/                Adapter interface + pip, npm, brew, cargo, go, tap, gem, apt, dnf, pacman, winget, choco implementations
internal/registry/embedded/       tap formulas built into the binary
internal/account/                 Supabase Auth client (login/plan/profile)
internal/selfupdate/              downloads + verifies + replaces the poly binary
installers/                       macOS .pkg and Windows .exe installer sources
site/                             marketing site, docs, account, community pages
scripts/build-all.sh              cross-platform release build

Status

Working: install/remove/list/search/info/upgrade/init across pip, npm, brew, cargo, go, tap, gem, apt, dnf, pacman, winget, and choco; version pinning (where the backend supports it); automatic self-update and (Pro) automatic package upgrades; macOS/Windows installers; a Supabase-backed account system with a community directory. Not yet built: signed/ notarized installers and a package search UI covering every adapter's full catalog (vs. exact-name lookups).