Introducing lazypkg – A Cross-Package Manager Tool for Effortless Updates

How do you manage packages installed via different package managers?
In my case, I use macOS for work and Linux for personal development, so I manage most of my tools and configurations (like dotfiles) using Nix flakes + Home Manager.
That said, for tools I just want to quickly try out or tools specific to a single project, I often end up installing them via brew
, npm install -g
, or apt
.
The problem is—once you’re juggling multiple package managers, it becomes hard to keep track of which packages are outdated. Plus, the command-line options vary across tools (was it update
or upgrade
…?).
So I Built a TUI App Called lazypkg
To solve this, I built lazypkg—a terminal-based tool that lets you view and update packages across multiple package managers.
https://github.com/ymtdzzz/lazypkg
We’ll dive into usage later, but here’s a quick overview of what it can do:
- List upgradable packages:
- Package name
- Current version
- Latest version
- Update individual packages
- Update all packages at once
As of March 22, 2025, the following package managers are supported (with a modular design to allow easy additions):
- apt
- gem
- homebrew
- npm
- docker
Docker support is disabled by default to avoid hitting Docker Hub API rate limits.
Getting Started
Installation
You can install lazypkg
via Homebrew or go install
.
# Homebrew
brew install ymtdzzz/tap/lazypkg
# go install
go install github.com/ymtdzzz/lazypkg@latest
Check if it’s installed:
lazypkg -v
# -> lazypkg version 0.0.7
Launch & View Upgradable Packages
Running lazypkg
automatically detects supported package managers, populates the sidebar, and fetches package updates.
If a package manager (like apt
) requires root, a password prompt will appear.
I updated everything before writing this post, so screenshots below use demo data.
Initially, focus is on the sidebar:
Here are the basic keybindings:
-
↑↓
orj/k
: Move focus up/down in the sidebar -
Enter
or→
orl
: Switch focus to the package list pane -
Space
: Add manager to bulk update targets -
r
: Refresh updates for the selected manager -
u
:- With no bulk target: Update all packages under current manager
- With bulk target(s): Update all selected managers’ packages
These operations mostly work the same in the package list pane as well.
Use Cases
Updating a Specific Package
Say you want to update only terraform
under homebrew
.
Focus on homebrew
in the sidebar, then press Enter
or →
or l
to move to the package list.
Focus on terraform
, press u
, and a confirmation dialog appears.
Press Enter
to execute. Logs appear in real-time at the bottom (ctrl+j
/ ctrl+k
to scroll).
The demo just fakes logs—actual command would be
brew upgrade terraform
.
Once done, the package list refreshes and removes terraform
from the upgrade list.
Updating Multiple Packages
You can select multiple packages with Space
. Let’s try selecting ffmpeg
, terraform
, and wget
.
Then press u
, confirm, and they’ll all be updated in one go.
Update All Packages under a Package Manager
To update all packages managed by, say, apt
:
Return focus to the sidebar with Backspace
, ←
, or h
, focus on apt, and press u
. That’s it.
Alternatively, pressing a
in the package list achieves the same thing.
That’s the gist of using lazypkg
. Next up, the motivation and implementation details.
Why Build This When Tools Like topgrade
Exist?
There are already some tools which have the same concept like topgrade
https://github.com/topgrade-rs/topgrade
It’s a fantastic tool and I used it for a while myself. But topgrade
doesn’t show you which packages can be updated or what the new versions will be before updating—it just updates everything.
While that simplicity is great, I wanted a tool that takes a slightly different approach.
When it comes to globally installed packages, despite the interface differences, our needs are often:
- View installed packages
- Check for updates
- Update selected packages
And frankly, I don’t care which manager is handling it—as long as something like lazypkg
abstracts over the differences, the UX becomes so much smoother.
I decided to skip the “list installed packages” feature, since just knowing what’s outdated is usually enough.
Implementation Notes
Still a bit rough, but I want to highlight some design choices. The tool is written in Go, which I’m most comfortable with.
Chose bubbletea
for TUI Framework
I’ve built other TUI tools, like otel-tui, which uses tview
.
otel-tui
needed a custom frame graph renderer, and tview
was perfect for that.
But this time, I wanted an Elm-style architecture with clear state transitions, so I chose bubbletea.
It wasn’t a heavily researched choice (it’s personal dev, after all), but I figured the event-driven model would better suit async tasks like package updates.
In bubbletea
, you modify state (Model) only via messages. For example, updating a package involves two messages:
// https://github.com/ymtdzzz/lazypkg/blob/85b8a4e01fb5c5a75797f530e8893e871759104b/components/messages.go#L27C1-L36C2
type updatePackagesStartMsg struct {
name string
pkgs []string
}
type updatePackagesFinishMsg struct {
name string
pkgs []string
err error
}
On update finish, I hide the loading spinner and re-fetch updates:
// https://github.com/ymtdzzz/lazypkg/blob/85b8a4e01fb5c5a75797f530e8893e871759104b/components/packages.go#L149-L157
case updatePackagesFinishMsg:
if msg.name == m.name {
for _, pkg := range msg.pkgs {
if i, ok := m.pkgToIdx[pkg]; ok {
m.loading[i] = false
}
}
cmds = append(cmds, m.getPackagesCmd())
}
Compared to tview, which offers a lot of flexibility and lets you structure state management your own way, bubbletea provides more built-in guidance with its Elm-style architecture—making it easier to follow a clear path, especially in personal projects.
Still, as the app grows, centralizing all messages in one file won’t scale. That’s a future refactor.
Extensible Design
Right now, only a few package managers are supported, but I’ve made it easy to extend.
Each package manager is implemented as an executor
that fulfills this interface:
// https://github.com/ymtdzzz/lazypkg/blob/85b8a4e01fb5c5a75797f530e8893e871759104b/executors/executor.go#L18-L39
// Executor defines the interface for package management operations
type Executor interface {
// GetPackages retrieves a list of available package updates.
// The password parameter is required for package managers that need elevated privileges.
GetPackages(password string) ([]*PackageInfo, error)
// Update performs an update operation on a single package.
// If dryRun is true, it will only simulate the update without making actual changes.
// The password parameter is required for package managers that need elevated privileges.
Update(pkg, password string, dryRun bool) error
// BulkUpdate performs update operations on multiple packages simultaneously.
// If dryRun is true, it will only simulate the updates without making actual changes.
// The password parameter is required for package managers that need elevated privileges.
BulkUpdate(pkgs []string, password string, dryRun bool) error
// Valid checks if the package manager is available and usable on the current system.
Valid() bool
// Close performs any necessary cleanup operations when the executor is no longer needed.
Close()
}
If you’re curious, here’s the Homebrew executor implementation, where brew outdated --verbose
output is parsed via regex (yep, kind of hacky).
As for the password
argument—I’ve built in a prompt that’s triggered automatically when elevated privileges are needed. The flow looks like this:
- Attempt command with empty password (expecting failure)
- Detect error output, show password dialog
- On user input, re-run command with provided password
Passwords aren’t stored in structs or memory unnecessarily—they only exist within specific function scopes.
Closing
lazypkg
is a pretty simple tool, but I use it every day and find it genuinely useful.
There are tons of features I’d like to add—UI polish, support for more managers—but it’s already functional and I plan to keep maintaining it.
If you face similar pain points with managing packages, I hope lazypkg
helps.
Feedback is more than welcome!