TL;DR: I kept writing the same Go CLI bootstrap code over and over: config loading, version checks, self-update, logging, auth. So I extracted it into a set of packages, used them on enough projects to be sure they were right, then forged them into a single library called go-tool-base and wrapped a code generator around it. Now a new Go CLI tool starts as a working, self-updating, self-documenting project instead of an empty main.go and a sigh.
The same afternoon, over and over
If you’ve written more than two or three command-line tools in Go, you’ll know the shape of the first afternoon. You reach for Cobra for the command tree, Viper for config, and then you start the part nobody puts in the README: the plumbing.
Where does config live? A file, an env var, an embedded default? In what order do they override each other? How does the tool tell the user there’s a newer version? How does it actually update itself? What does logging look like, and is it the same logging the next tool will use? How do you wire all of that into each command without every command reaching into a pile of globals?
None of it is hard. That’s the problem. It’s not hard, it’s just there, every single time, and every single time I found myself reinventing it slightly differently to the last time. Different override precedence here. A subtly different update flow there. Logging that didn’t quite match the tool I wrote three months earlier. Each tool was a fresh re-litigation of decisions I’d already made.
The Boy Scout rule I’ve banged on about for years (leave the codebase better than you found it) has an uncomfortable corollary. If you keep finding the same campsite in the same mess, at some point the honest thing to do is stop tidying it and go build a better campsite.
First, just packages
So I started pulling the recurring pieces out into their own packages. Nothing grand. A config package that did the hierarchical merge the way I always ended up doing it. A version package that knew how to compare semver and detect a development build. A setup package that handled first-run bootstrap and self-updating from a release. They lived as separate repositories, and if you go digging through my GitHub history you can still find the ancestors of them scattered about.
Separate packages was the right first move. It forced each piece to stand on its own and earn its keep on a real project before I trusted it on the next one. A package that’s only ever used in the repo it was born in hasn’t really been tested. It’s just been agreed with.
But separate packages have a tax. Each one has its own release cadence, its own changelog, its own CI. Worse, they have to agree with each other at the seams, and when they’re versioned independently those seams drift. I’d bump the config package, and the setup package that depended on it would quietly need a matching bump, and the tool that used both would need to be told about both. I’d traded “reinvent the wheel” for “keep a dozen wheels in sync.” That’s not obviously a better deal.
Then, one library
Once the packages had been used enough (used in anger, on real tools, by people who weren’t me), the shape of them stopped moving. The interfaces settled. The arguments about precedence and defaults were over because the answers had survived contact with reality.
That’s the point at which separate packages stop being a virtue and start being friction. So I forged them into one: go-tool-base. One module, one version number, one changelog, one set of seams that are now internal and can’t drift because they ship together.
The heart of it is a dependency-injection container, a Props struct, holding the things every command needs: the logger, the config, the embedded assets, the filesystem handle, the error handler, the tool’s own metadata. Commands receive Props explicitly rather than reaching for globals, which means a command is just a function of its inputs and is therefore trivially testable. That one decision quietly pays for itself on every tool since.
Around that container sits the stuff I was tired of rewriting: hierarchical config, structured logging, version checking, self-update from GitHub or GitLab releases, an interactive TUI documentation browser, AI integration, service lifecycle management. A new tool inherits all of it and gets to spend its first afternoon on the thing that’s actually novel: its own logic.
Finally, a generator
A library still leaves you with a blank main.go. You still have to know the conventions, wire the container, lay out the directories, register the commands. Knowable, but boilerplate. And boilerplate is exactly the enemy I set out to kill.
So go-tool-base ships a generator. gtb generate skeleton scaffolds a complete, working, idiomatic project: directory layout, the wired Props container, the command tree, CI, the lot. gtb generate command adds a new command and registers it for you. The generator also handles upkeep: when the framework’s conventions move, it can regenerate the scaffolding of an existing project without trampling the code you’ve written on top. (That last part turned out to be its own interesting problem, and a future post.)
The goal is blunt. Creating a CLI tool should be about the tool, not the scaffolding. The first afternoon should be spent on the part that’s worth writing.
One thing I was careful about
There’s a failure mode with “batteries-included” frameworks: the day you outgrow them, they hold you hostage. You either stay inside the framework’s worldview forever or you face a rewrite.
go-tool-base generates idiomatic, standard-library-compliant Go. There’s no magic runtime you can’t see, no code you couldn’t have written by hand. If you ever outgrow the framework, the generated code stands on its own and you walk away with a normal Go project. A framework should be a starting point you’re glad you took, not a room you can’t leave.
The upshot
go-tool-base exists because I was spending the first afternoon of every Go CLI tool rebuilding the same plumbing, and rebuilding it slightly wrong relative to last time. It started life as separate packages so each piece could earn its place on real projects; once they’d stopped moving, I forged them into a single library so the seams couldn’t drift; and I wrapped a generator around it so a new tool starts as a working project rather than a blank file.
It’s a framework for the unglamorous 80% (config, versioning, updates, logging, lifecycle) so you can spend your time on the 20% that’s actually yours.
Over the coming posts I’ll dig into the individual pieces: the generator that won’t clobber your edits, the credential handling, the self-update integrity checks, and a few Go techniques I’m rather pleased with along the way. Stay tuned.