The Nightmare of Developer Onboarding
We’ve all been there. You get a fresh Linux machine, or you join a new team, and the onboarding process is a massive, outdated README.md file.
You spend the next three hours blindly copy-pasting terminal commands to install Git, Docker, Go, SSH keys, and various system dependencies. Halfway through, a command fails because your system architecture is slightly different, or a package manager changed its syntax.
Historically, teams try to solve this with massive Bash scripts (setup.sh). But Bash scripts are incredibly brittle. They lack robust error handling, they fail silently, and when they break, you are left staring at a wall of cryptic text.
I wanted to build something better. A tool that validates a Linux environment, clearly reports what is missing, and safely orchestrates the installation of essential tools. That’s why I built EnvGuard.
The Tech Stack: Why Go and Bubble Tea?
I chose Golang because it solves the ultimate “chicken-and-egg” problem of bootstrapping environments. If you write a setup script in Python or Node.js, the user has to install Python or Node just to run the script!
Go compiles down to a single, static, dependency-free binary. You can drop the EnvGuard executable onto a completely bare-bones Linux machine, and it will just run.
But I didn’t just want it to execute commands; I wanted it to feel like a modern application. To fix the ugly terminal output, I used Bubble Tea (by Charmbracelet). Bubble Tea brings the Elm architecture to Go, allowing you to build reactive, state-driven user interfaces right inside the terminal.
The User Experience: Bringing UI to the Terminal
Because of Bubble Tea, the user experience of EnvGuard is entirely guided. When you run the tool, you are greeted by a Welcome Screen. Pressing Enter transitions the state to an interactive checklist where you can navigate and select exactly which tools you want to validate.
Behind the scenes, instead of writing messy loop logic, Bubble Tea uses a beautiful Elm-inspired update loop. By structuring it this way, the UI reacts instantly to user input, updating the terminal screen without ever clearing it or printing messy logs.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 { m.cursor-- }
case "down", "j":
if m.cursor < len(m.choices)-1 { m.cursor++ }
case "enter", " ":
_, ok := m.selected[m.cursor]
if ok { delete(m.selected, m.cursor) } else { m.selected[m.cursor] = struct{}{} }
}
}
return m, nil
}
Conceptual snippet of the Bubble Tea update loop.
Role‑Based Onboarding: Smarter Defaults
One of the most powerful additions to EnvGuard is the role selection screen. Instead of presenting a flat list of tools, EnvGuard first asks the user about their primary role, for instance:
- Developer: Git, Docker, and SSH.
- Designer: Git (and Docker if needed).
- SysAdmin: Docker and SSH.
This is implemented by expanding the Bubble Tea model to manage multiple screens (welcome, role selection, tool selection). The state machine tracks the current screen and the chosen role:
type Screen int
const (
WelcomeScreen Screen = iota
RoleScreen
ToolsScreen
)
type model struct {
currentScreen Screen
roleChoices []string
chosenRole int
tools []string
selectedTools map[int]struct{}
// ... other fields
}
When the user picks a role (e.g., “Developer”), the model calls an applyRecommendations() function that populates the selectedTools map with indices of recommended tools.
This dramatically improves the experience: they get a sensible starting point without having to know every tool essential for their role, yet they remain in full control.
The Architecture: Separation of Concerns
I wanted the architecture to be clean, with a strict separation of concerns. Looking at my project structure, you can see how the application is divided:
cmd/— Houses the Cobra CLI definitions and flags.tui/— Handles all visual state and terminal rendering.checks/— Contains actual OS-level execution logic.
When the user selects a tool to validate, the CLI orchestrator handles the routing, but delegates the heavy lifting to the checks package.
// Conceptual: The Orchestration Flow
func checkTool(tool string) {
// 1. Delegate the OS-level check to our isolated 'checks' package
isInstalled, _ := checks.Verify(tool)
if !isInstalled {
fmt.Printf("✘ %s is missing. Attempting automated fix...\n", tool)
// 2. Trigger the OS-specific package manager (e.g., dnf)
success, _ := checks.Fix(tool)
if success {
fmt.Printf("✔ Successfully installed %s!\n", tool)
} else {
fmt.Printf("Error: Manual intervention required.\n")
}
} else {
fmt.Printf("✔ %s is already installed.\n", tool)
}
}
What’s Next: Interfaces & Going Cross-Platform
Right now, EnvGuard is heavily optimized for my personal workflow on Linux. But the ultimate goal is universality. My next major milestones are:
- Refactoring to Interfaces: I plan to abstract the switch statement into a standard Go
interface(e.g.,type ToolValidator interface). This will make adding new tools completely modular. - Cross-Platform Support: Implementing OS-detection logic (
runtime.GOOS). Soon, whether it’sdnfon Fedora,apton Ubuntu, orbrewon macOS, EnvGuard will automatically adapt.
Conclusion
Building EnvGuard was a masterclass in combining system-level orchestration with modern UI principles. It proved to me that terminal tools don’t have to be archaic and brittle…they can be robust, beautiful, and deeply helpful.