
Building a React-based CLI to Speed Up Development | Part 1: What + Why
As a developer, have you ever felt like your terminal tabs were multiplying faster than you could close them, leaving you in a tangled mess of processes and forgotten ports? We’ve been there. Every morning, our development workflow started with a frustrating dance of opening and managing dozens of terminal tabs just to get various services and applications up and running. But what if there was a better way?
The Problem
At PressBox, we have a monorepo with React Router/Remix apps, Cloudflare Workers, multiple database servers, and containerized services that all need to run in our local environment. Starting work meant opening/resuming tab after tab in the terminal:
# Tab 1
cd apps/pressbox && pnpm dev
# Tab 2
cd apps/grandstand && pnpm dev
# Tab 3
cd apps/admin && pnpm dev
# Tab 4
cd workers/curator && pnpm wrangler dev - port 8787
# Tab 5
cd workers/inbox && pnpm wrangler dev - port 8788
# Tab 6–15
# ... you get the ideaBy lunch, you’ve got tabs everywhere. You’re command-tabbing through windows trying to find the worker that’s throwing errors, and then you accidentally close the wrong tab. Now you spend 5 minutes figuring out what just died, restarting it, and hoping you got the right port.
We needed to fix this. Ideally, the solution could live in the command line, where we all spend a lot of time in our development process, but we also weren’t in a hurry to write and maintain a collection of Bash scripts.
The Solution: PBX CLI
What if we had one customized dashboard instead of dozens of terminal tabs? One place that shows everything at a glance, where you can press a key to start or stop services, can view logs of all services together, easily create and apply database migrations, and a whole lot more.
After using Claude Code and other modern CLI tools built using Ink, we saw how good modern terminal interfaces can be. We considered our local environment and tried out Ink for a few smaller scripts, and that’s when it really clicked: using React and TypeScript was a pretty powerful way to create CLI tools.
Why This Works
A Monorepo Sure Helps
PBX works particularly well because at PressBox, we use a monorepo. Almost everything lives in one place: our React Router apps, Cloudflare Workers, database schemas, and even some containerized services. This means PBX can understand our entire system architecture simply by parsing the file structure - no service registries, no configuration files scattered across repositories, no manual coordination.
This approach has some caveats, though. Since we’re not running everything in containers, it assumes everyone on your team uses similar development environments (we’re all on macOS) and has the same system dependencies installed. If you’re dealing with microservices across multiple repos, or if you have a team split between Mac, Windows, and Linux, you’d probably need to rely more heavily on containerization under the hood — though you could still use a layer of Ink on top.
React Knows State Management and UI
For us, the monorepo structure combined with React’s state management was perfect. Managing services is fundamentally a state management problem: services are either running or stopped, logs stream in real-time, and ports need to be allocated without conflicts. This is something that works pretty well with built-in React state management.
Once we started with Ink, suddenly we could build real interfaces with proper navigation with arrow keys, color-coded statuses that update in real-time, and live streaming logs. Most of the web development patterns we were already familiar with actually worked in the terminal.
The Killer Features
Automatic Service Discovery
Another nice time saver: PBX automatically discovers new services. Add a new app to the apps/ directory, a worker to workers/, or a container to containers/, and PBX finds it immediately. When someone adds a new service, everyone else gets it automatically the next time they run PBX. No configuration changes are needed to update a service registry, and no Slack messages are required about new port allocations. It just appears in your dashboard.
Session Saving/Restoration
This turned into one of our favorite features. When you exit PBX, it automatically saves your session (which services were running) in a simple JSON file. Come back later (whether it’s after lunch or the next morning), and if that JSON file exists, PBX asks if you want to restore your previous session:
Press enter or click to view image in full size
PBX Session Restoration
Simply press Enter and you’re back where you left off. No hunting for commands, no remembering which services need to run again. This is huge when you’re context-switching between projects, starting back up after a reboot, or coming back from vacation.
No More Port Conflicts
Port conflicts were getting tough to deal with, too. Someone would hardcode port 3000 for their service or database server, then someone else would use it for another service, and suddenly, nothing works. We fixed this with hash-based deterministic ports:
Get Andy Crum’s stories in your inbox
Join Medium for free to get updates from this writer.
Remember me for faster sign in
Now pressboxalways gets port 8094 and grandstandgets 8112. Same port every time, no conflicts, and we don’t have to maintain a spreadsheet of who’s using what. This isn’t a perfect solution (the risk of conflicts increases as you add running services), but it’s worked pretty well at our scale.
Database Migrations That Don’t Suck
One of the best parts of PBX is its handling of database migrations with Drizzle. Instead of memorizing complex migration commands and flags, we built a visual dashboard that makes things clear:
Press enter or click to view image in full size
DB Migration Management in PBX
Some helpful features:
Creating a migration? PBX automatically detects when your schema changes and shows you exactly what will change before you commit to it.
Need to update your database? Apply migrations to local or production databases with a single keypress.
Is your migrations list getting long and cumbersome? Easily squash multiple migrations together when things get messy.
When you generate a migration, PBX runs the schema diff, shows you the actual SQL that will execute, asks for a descriptive name, creates the file in the appropriate place, and updates the migration index. No more typing drizzle-kit generate:sqlite --schema=./src/schema.ts --out=./migrationsand hoping you got the flags right. It just works.
We also added smart migration warnings right in the status bar, so you always know when there are pending migrations for your databases:

PBX Status Bar, a nice spot to add important data/notifications
All Logs in One Place
Another key feature of PBX is the ability to see logs from all services in one place. When something breaks across multiple workers and you need to trace a request through your system, you can see everything that’s happening, with the relevant service name next to each entry. Having logs from every service easily viewable within PBX without having to switch tabs from service to service is a huge advantage.
The Impact
Life before PBX:
Environment setup took 5–7 minutes of hunting for the right commands
Everyone had 5–15 terminal tabs open at any given time
We spent way too much time going through tabs looking for the right logs
Life after PBX:
Morning setup is typing
pbxand pressing Enter (5 seconds flat)We need exactly one terminal tab
All logs are in one spot
But the real win isn’t just the time savings; it’s that you don’t have to think about local infrastructure anymore. Your brain is free to focus on actually building things instead of managing terminal tabs.
What We Learned
React works great for CLIs. After working with it for a few minutes, we knew Ink was the right choice. Our team already knows TypeScript and React, so now we can build CLI features very easily and quickly.
Direct process management beats containers for local dev (with caveats). This works great for us because we’re all on macOS with the same Node versions and system dependencies. Managing processes directly is faster, uses less memory, and has zero Docker Desktop overhead. Again, if you’re running different language runtimes or your team uses different operating systems, containers are still going to be really helpful, but a wrapper like PBX on top of those containers could still be worth considering.
Monorepos create a lot of possibilities. Having everything in one place means PBX can understand your entire system just by reading the filesystem — no API calls to service registries, no syncing configuration between repos. When everything lives together, your tooling can be smarter.
Small things matter. Session restore, good defaults, and consistent keyboard shortcuts — they all add up to a much better experience. Since developers are the users, it’s easy to solve “paper cuts” and general time-wasting activities when you have a framework like this for dev tooling.
The Bottom Line
Building something custom with React for CLIs might sound weird at first, but for us, it made a lot of sense. We went from terminal tab bloat to one clean interface that is extensible and just works. Developer setup went from hunting for commands and remembering ports to simply typing three characters.
The best developer tools are those that are built off-the-shelf and solve your exact problem with little to no effort. The second-best developer tools are the ones that you build yourself using the technologies you already know to directly solve the problems you have. When your whole team knows the tech stack, everyone can contribute to making the tool better and improving the whole experience; that’s how a tool becomes a living project that evolves with your needs.
Next Time…
In Part 2, we’ll dive deeper into the technical implementation — how we built the process management system, implemented service discovery, how the UI works, and more. Stay tuned!