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: ↑↓ or j/k: Move focus up/down in the sidebar Enter or → or l: 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. https://github.com/rivo/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

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!