Development
-
CLI First, GUI Never
I’ve been enjoying building CLI tooling and so here’s why your next app should be a CLI not a GUI.
CLI tools have longevity. If you design them for composability, there’s a good chance they’ll stick around because they’re much easier to plug into an existing ecosystem of other CLI tools. GUIs on the other hand come and go, as aesthetics change and as popular UI libraries rise and fall.
I’ll be honest, this is just a thinly veiled excuse for myself to explore building CLIs in different languages. I’ve built them in TypeScript with Bun and I’ve built them in Python, but TUIs in Go really changed the game for me on what is possible.
Tips for Building CLIs That Last
Output JSON Structured output is way easier to parse with scripts, and agents appreciate the additional context that JSON provides. If you’re building tools in 2026, you’re building them for humans and machines.
Wrap existing CLIs instead of re-implementing their APIs. You’re trading a raw API dependency for a versioned, maintained interface. Someone else is absorbing the upstream churn.
Prefer stdin/stdout over files where possible. This works better if you ever want to containerize your tool, and it plays nicely with Unix piping.
Logging matters. This kinda goes with JSON but any sort of logging is so important I’ll add it twice. Having some logging is non-negotiable, but structured logging really matters if you’re sending logs to a centralized provider.
Single binaries are way easier to distribute than a zip file or a bunch of code someone has to set up. It’s fairly straightforward to set up GitHub auto-releases, though there are some steps that can trip you up. One approach: auto-create a new patch version on every commit to main.
Three CLIs I Built (For Inspiration)
Here are some personal examples to hopefully inspire you to build your own:
-
lsm — A local secrets manager. Instead of
.envfiles sitting on disk, it decrypts and optionally injects secrets into your application runtime.lsm exec -- pnpm dev -
repjan — A Go TUI that wraps the
ghCLI to help you manage all your old repos. -
positive.help CLI — Personal tooling for managing a website entirely from the command line. No admin dashboard needed.
The Agentic Argument
In the age of agentic development, CLIs that your agents can call are incredibly useful. An agent can call a CLI a lot easier than they can click buttons in a GUI.
A GUI usually requires browser automation, maybe some scraping. It’s getting a bit easier now with APIs that return markdown from a site, but not everybody knows how to use those tools, and there’s usually a cost.
If you want a signal, look at the adoption curves of agent-focused CLI tools over the last six months. GitHub stars aren’t a perfect metric, but the direction is hard to argue with.
So this is your sign, you don’t need a framework. You don’t need a design system. If it’s good enough for
grep, it’s good enough for your tool. -
-
obra/superpowers: An agentic skills framework & software development methodology that works.
An agentic skills framework & software development methodology that works. - obra/superpowers
-
Just: The Command Runner
If you’ve ever used
maketo run commands, it works, but you’re using a build system as a command runner. Just is a dedicated command runner that does exactly what you want and doesn’t come with all the baggage.Created by Casey Rodarmor about a decade ago (it turns 10 this June), Just is written in Rust and laser-focused on being a really good command runner. That’s it; just a clean way to define and run commands.
Why I Use It
On a lot of my projects, I use Just in conjunction with Mise. Mise does a fine job as a task runner, but when I want something more expressive for command orchestration, Just is what I reach for.
For example, I’ve got a lot of really long Python commands these days. I’m injecting environment variables at runtime, calling Poetry, then running Python with specific arguments. There’s a ton of boilerplate you have to type out every time. I don’t want to remember all of that, and I definitely don’t want to go digging through my shell history or copy-pasting from a README.
Just simplifies that. Define it once in a
justfile, and now your common commands are nowjust <recipe>.Modules Changed the Game
Something I started using recently is Just’s module system, which with it you can create separate
.justfiles and have your mainjustfileimport them as submodules. This lets you group related commands naturally.So instead of flat recipes like:
just test-cover just test-unit just test-integrationYou can organize them into modules:
just test cover just test unit just test integrationIt’s a small change, but it makes your command surface feel a lot more intuitive, especially as your project grows and you accumulate dozens of commands.
Getting Started
If you want to dig deeper, Casey wrote a great walkthrough called Tour de Just that covers the highlights. The official documentation is great too.
Just is commercially friendly, widely packaged, and easy to install on pretty much any platform. If you find it useful, you should consider sponsoring Casey on GitHub. Open source maintainers deserve support for tools we rely on daily.
-
I'm Just Going to Use Zed
I’ve been thinking about my editor setup and I wanted to work through the costs out loud. This post is mainly for me, but maybe it helps you too.
Here’s where I landed: I’m canceling(ed) JetBrains and Cursor, and just using Zed.
I know that might sound crazy. JetBrains All Products has been my go-to for a while now, and Cursor was interesting for a bit, but turns out all I needed was speed and cheap.
The Cost Breakdown
Editor Monthly Cost Yearly Cost Zed Pro $10.00 $120.00 Cursor Pro $20.00 $240.00 ($192 if billed annually) JetBrains All Products ~$14.92 $179.00 Zed Pro at $10/month is the cheapest option by a decent margin. That’s $120 a year versus $179 for JetBrains or up to $192 for Cursor. Not a huge difference money, but it adds up.
Zed Is Enough
My primary way of working these days is Zed plus CLI-based tools. I use Claude Code in the terminal (Mostly Ghostty) alongside the IDE, and that combination handles everything. The editor itself is fast, the AI integration is solid.
I already canceled Cursor. It’s a good product, but not at double what Zed costs. Maybe I’ll go back and try it again at some point, but right now there’s no reason to do that.
Sometimes the right move is just picking what works and not overthinking it.
-
Why Ghostty Is My Terminal for Agentic Work
I love Ghostty for agentic work (mostly Claude Code). It doesn’t try to bake in its own agentic coding environment. It’s completely unopinionated about how you use it. It is exactly what I want from a terminal.
It’s open source, primarly made by one person, Mitchell Hashimoto, who doesn’t ask you for any money. No outside investment, no employees. Just a really solid (I think the best?!) terminal emulator.
Sometimes I do wish it had slightly better navigation, and system notifications was easier to figure out, but this is minor stuff and not a blocker for me being productive or enjoying the work.
The Warp’ed De-Tour
I used to use Warp before Ghostty. I’ll still open it occasionally to see what they’re working on. Warp has some interesting ideas, they’re trying to replace your IDE and be your entire agentic development environment. The problem is they seem to have too many features now for general use. I think this approach will turn off both the IDE crowd and the Neovim crowd simultaneously. So, I keep going back to Ghostty.
We now have a new contender.
Enter Cmux
Cmux is a newer option that actually solves those two minor problems I had with Ghostty. It has better navigation with side tabs, and notifications work out of the box. It’s open source and free to use, and it’s built on Ghostty under the hood, so the core terminal experience is solid.
There’s a small AI company behind it. It looks like their Y Combinator batch was in 2024, and they’re trying to build some kind of product on top of Cmux, possibly memory-related. Though with Claude Code getting better at memory and plenty of free memory frameworks already out there, I’m not sure where that is headed. This CMUX project could be the start of a pivot?
The repo is kind of a mess, they have their website mixed in with the application code. And they offer something called a “Founder’s Edition” for $30/month… which I don’t know how that makes any sense when Warp is $20/month, Zed is $10/month, and Cursor is $20/month.
However it’s optional and the free version of Cmux is really good right now; but I’m be doubtful it exists in five or ten years. My guess is their exit strategy is to get acquired by a model provider, given that they have taken investment.
I am having fun with cmux, so check it out if you haven’t yet!
-
Claude Code Skills vs Plugins: What's the Difference?
If you’ve been building with Claude Code, you’ve probably seen the terms “skill,” “plugin,” and “agent” thrown around. They’re related but distinct concepts, and understanding the difference will help you build better tooling. Let’s focus on skills versus plugins since those two are the most closely related.
Skills: Reusable Slash Commands
Skills are user-invocable slash commands, essentially reusable prompts that run directly in your main conversation. You trigger them with
/skill-nameand they execute inline. They can be workflows or common tasks that are done frequently.Skills can live inside your
.claude/skills/folder, or they can live inside a plugin (where they’re called “commands” instead). Same concept, different home.The important frontmatter you should pay attention to is the
allowed-toolsproperty. This defines which tool calls the skill can access, and there are three formats you can use:- Comma-separated names —
Bash, Read, Grep - Comma-separated with filters —
Bash(gh pr view:*), Bash(gh pr diff:*) - JSON array —
["Bash", "Glob", "Grep"]
I don’t think there’s a meaningful speed difference between them? The filtered format might take slightly longer to parse if you have a huge list, but in practice it’s negligible. Pick whichever is most readable for your use case.
The real power here is that skills can define tool calls and launch subagents. That turns a simple slash command into something that can orchestrate complex workflows.
Plugins: The Full Package
A plugin is a bigger container. It can bundle commands (skills), agents, hooks, and MCP servers together as a single distributable unit. Every plugin needs a
.claude-plugin/plugin.jsonfile; which is just a name, description, and author.Plugins are a good way to bundle agents with skills. If your workflow needs a specialized agent that gets triggered by a slash command, a plugin is a good option for that.
Pushing the Boundaries of Standalone Skills
However, I wanted to experiment with what’s actually possible using standalone skills, so I built upkeep. It turns out that you can bundle actual compiled binaries inside a skill directory and call them from the skill. That opens up a lot of possibilities.
Here’s how I did it:
- The skill has a prerequisite section that checks for a
bin/folder containing the binary - A workflow calls the binary, passing in the commands to run
- Each step defines what we expect back from the binary
You can see the full implementation in the SKILL.md file. It’s a pattern that lets you distribute real functionality, not just prompts, through the skill.
Quick Summary
- Skills are slash commands. Reusable prompts with tool access that run in your conversation.
- Plugins bundle skills, agents, hooks, and MCP servers together with a
plugin.json. - Skills are more flexible than you might expect, you can call subagents, distribute binaries, and build real workflows.
If you’re just getting started, skills are the easier entry point. When you need to package multiple pieces together or distribute agents alongside commands, that’s when you reach for a plugin.
Have fun building!
- Comma-separated names —
-
You move fast. Cloud development cycles do not.
Mixing and matching optimization strategies won’t fix your slow development loop. LocalStack streamlines your feedback loop, bringing the cloud directly to your laptop. Same production behavior. Faster feedback. Fully under your control.
-
Serverless and Edge Computing: A Practical Guide
Serverless and edge computing have transformed how we deploy and scale web applications. Instead of managing servers, you write functions that automatically scale from zero to millions of users.
Edge computing takes this further by running code geographically close to users for minimal latency. Let’s break down how these technologies work and when you’d actually want to use them.
What is Serverless?
Serverless doesn’t mean “no servers”, it means you don’t manage them. The provider handles infrastructure, scaling, and maintenance. You just write “functions”.
The functions are stateless, auto-scaling, and you only pay for execution time. So what’s the tradeoff? Well there are several but the first one is Cold starts. The first request after idle time is slower because the container needs to spin up.
The serverless Platforms as a Service are sticky, preventing you easily moving to another platform.
They are stateless, meaning each invocation is independent and doesn’t retain any state between invocations.
In some cases, they don’t run Node, so they behave much differently when building locally, which complicates development and testing.
Each request is handled by a NEW function and so you can imagine that if you have a site that gets a lot of traffic, and makes a lot of requests, this will lead to expensive hosting bills or you playing the pauper on social media.
Traditional vs. Serverless vs. Edge
Think of it this way:
- Traditional servers are always running, always costing you money, and you handle all the scaling yourself. Great for predictable, high-traffic workloads. Lots of different options for hosting and scaling.
- Serverless (AWS Lambda, Vercel Functions, GCP Functions) spins up containers on demand and kills them when idle. Auto-scales from zero to infinity. Cold starts around 100-500ms.
- Edge (Cloudflare Workers, Vercel Edge) uses V8 Isolates instead of containers, running your code in 200+ locations worldwide. Cold starts under 1ms.
Cost Projections
Here’s how the costs break down at different scales:
Requests / Month ~RPS (Avg) AWS Lambda Cloudflare Workers VPS / K8s Cluster Winner 1 Million 0.4 $0.00 (Free Tier) $0.00 (Free Tier) $40–$60 (Min HA Setup) Serverless 10 Million 4.0 ~$12 ~$5 $40–$100 Serverless 100 Million 40 ~$120 ~$35 $80–$150 Tie / Workers 500 Million 200 ~$600 ~$155 $150–$300 VPS / Workers 1 Billion 400 ~$1,200+ ~$305 $200–$400 VPS / EC2 The Hub and Spoke Pattern
Also called the citadel pattern, this is where serverless and traditional infrastructure stop competing and start complementing each other. The idea is simple: keep a central hub (your main application running on containers or a VPS) and offload specific tasks to serverless “spokes” at the edge.
Your core API, database connections, and stateful logic stay on traditional infrastructure where they belong. But image resizing, auth token validation, A/B testing, geo-routing and rate limiting all move to edge functions that run close to the user.
When to Use Serverless
- Unpredictable or spiky traffic — APIs that go from 0 to 10,000 requests in minutes (webhooks, event-driven workflows)
- Lightweight, stateless tasks — image processing, PDF generation, sending emails, data transformation
- Low-traffic side projects — anything that sits idle most of the time and you don’t want to pay for an always-on server… and you don’t know how to setup a Coolify server.
- Edge logic — geolocation routing, header manipulation, request validation before it hits your origin
When to Use Containers / VPS
- Sustained high traffic — once you’re consistently above ~100M requests/month, a VPS is cheaper (see the table above)
- Stateful workloads — WebSocket connections, long-running processes, anything that needs to hold state between requests
- Database-heavy applications — connection pooling and persistent connections don’t play well with serverless cold starts
- Complex applications — monoliths or microservices that need shared memory, background workers, or cron jobs
The Hybrid Approach
The best architectures often use both. It depends on your specific use case and requirements. It depends on the team, the budget, and the complexity of your application.
Knowing the tradeoffs is the difference between a seasoned developer and a junior. It’s important that you make the right decisions based off your needs and constraints.
Good luck and godspeed!
-
Ditch Jest for Vitest: A Ready-Made Migration Prompt
If you’ve ever sat there watching Jest crawl through your TypeScript test suite, you know pain. I mean, I know your pain.
When Switching to Vitest, and the speed difference is genuinely dramatic. The answers to why it’s slow are easy to figure out. There’s plenty of explanations on that, so I’ll leave that to you to go look up why.
I put together a prompt you can hand to Claude (or any AI assistant) that will handle the migration for you. Let me know how it goes!
The Migration Prompt
Convert all Jest tests in this project to Vitest. Here's what to do: ## Setup 1. Remove Jest dependencies (`jest`, `ts-jest`, `@types/jest`, `babel-jest`, any jest presets) 2. Install Vitest: `pnpm add -D vitest` 3. Remove `jest.config.*` files 4. Add a `test` section to `vite.config.ts` (or create `vitest.config.ts` if no Vite config exists): import { defineConfig } from 'vitest/config' export default defineConfig({ test: { globals: true, }, }) 5. Update the `test` script in `package.json` to `vitest` ## Test File Migration For every test file: 1. Replace imports — Remove any `import ... from '@jest/globals'`. If `globals: true` is set, no imports needed. Otherwise add: import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' 2. Replace `jest` with `vi` everywhere: - jest.fn() → vi.fn() - jest.mock() → vi.mock() - jest.spyOn() → vi.spyOn() - jest.useFakeTimers() → vi.useFakeTimers() - jest.useRealTimers() → vi.useRealTimers() - jest.advanceTimersByTime() → vi.advanceTimersByTime() - jest.clearAllMocks() → vi.clearAllMocks() - jest.resetAllMocks() → vi.resetAllMocks() - jest.restoreAllMocks() → vi.restoreAllMocks() - jest.requireActual() → vi.importActual() (note: async in Vitest) 3. Fix mock hoisting — vi.mock() is hoisted automatically, but variables used in the mock factory must be prefixed with `vi` or declared inside the factory. 4. Fix jest.requireActual — This becomes vi.importActual() and returns a Promise: // Jest jest.mock('./utils', () => ({ ...jest.requireActual('./utils'), fetchData: jest.fn(), })) // Vitest vi.mock('./utils', async () => ({ ...(await vi.importActual('./utils')), fetchData: vi.fn(), })) 5. Snapshot tests work the same way. No changes needed. 6. Timer mocks — same API after the jest → vi rename. 7. Module mocks with __mocks__ directories work identically. ## TypeScript Config Add Vitest types to tsconfig.json: { "compilerOptions": { "types": ["vitest/globals"] } } ## After Migration 1. Delete leftover Jest config files 2. Update CI config to use `vitest run` 3. Run tests and fix any remaining failures
If you’re still on Jest and your TypeScript test suite is dragging, give Vitest a shot. The migration is low-risk, the speed improvements are real, and with the prompt above, the hardest parts are handled. ☕
-
Polar — Monetize your software with ease | Polar
Monetize your software with ease