PATTY CLI started as a bash script I called deploy.sh. It was 50 lines, hardcoded to one server, and broke whenever the server's IP changed. It was also the most useful thing I wrote that month.

I was managing three different projects on three Linux VPS instances. Each one had a different deploy process: one used Docker Compose and needed a docker compose up -d --build, another was a Node.js app that just needed git pull && npm run build && pm2 restart, and the third was a static site that only needed rsync. Every deploy meant SSHing in, remembering which commands to run, and hoping I did not forget a step.

The bash script automated one project. It worked. I wanted the same for the others. But copying and modifying the script for each project was already starting to smell like the wrong approach. The config values (server IP, deploy path, compose project name) were hardcoded. Every variation meant a fork.

The first real version

I rewrote it in Node.js over a weekend. The motivation was simple: I needed a config file. Node.js made it easy to parse JSON, run shell commands, and handle async subprocesses without the hair-pulling of bash arrays and error handling.

The first Node.js version had three commands: init (create a config file), deploy (rsync files and restart the app), and status (SSH in and check that the server was alive). It worked for my projects. That was the bar.

I did not design an architecture. I did not plan for extensibility. I did not write tests. I wrote the minimum code that made my deploys consistent and stopped caring about the problem. That initial version is still in the git history, and looking at it now, it is embarrassing. It is also the reason the tool exists at all. If I had waited until I had a proper design, I would still be waiting.

Real needs drove every feature

Every feature in PATTY CLI came from a specific problem I hit while using it. Nothing was speculative.

Rollbacks. I deployed a breaking change to a production server at 11pm. The fix took five minutes — but those five minutes felt like an hour because I was SSHed in, manually reverting files, restarting services, and hoping I did not miss anything. The next day I added patty rollback production. It deploys from a git archive, so rolling back to any commit is a single command.

Server diagnostics. A server ran out of disk because of log rotation I forgot to configure. I did not notice until the app started returning 500s. patty status started as a quick health check. It grew into a 50-point security and health audit because every time a server had a problem, I added a check for it. Disk usage, firewall status, fail2ban, Docker security, swap — if I had to SSH in and check it manually once, I automated it.

Backups. A client asked "do we have backups of the database?" I realized I did not have a good answer. patty backup production was built that weekend. It dumps the database server-side and downloads a copy locally. The restore command came two weeks later when I needed to test that the backups actually worked (a lesson I learned the hard way).

Observability. Setting up Grafana and Prometheus from scratch takes about two hours if you know what you are doing. I did it three times across three projects before I automated it. Now patty obs provisions a full observability stack on any server in one command.

What did not happen

I did not build a CLI framework. I used commander (the most popular Node.js CLI library) and moved on. No custom argument parsing, no plugin system, no middleware pipeline. Just straightforward command handlers that call shell commands over SSH.

I did not build a server agent. The tool works entirely over SSH. The server does not know PATTY exists. This was not a design principle I arrived at through careful reasoning — it was the path of least resistance. SSH was already configured. Adding an agent would have meant installing, updating, and monitoring another service. I skipped it because I was lazy, and that turned out to be the right call: zero installation, zero background processes, zero security surface beyond what SSH already provides.

I did not write a design doc. I do not think every project needs one. When the scope is "automate the thing I do manually," the fastest path to the right design is to build it, use it, and iterate. The design emerges from the iterations.

The shape that emerged

After two years of use, PATTY CLI has a clear architecture: a .deploy/config.json file in each project, environment-specific overrides, SSH multiplexing for connection reuse, and a local state file that tracks deployments. Each command is independent — you can use deploy without ever using backup or obs.

The architecture was not designed. It was discovered. Every feature added a constraint, and the constraints shaped the system. The result is simpler than anything I could have designed upfront because I did not have to account for hypothetical future needs — only the ones that actually showed up.

PATTY CLI now handles deploys, rollbacks, backups, server diagnostics, auto-fixes, container management, file uploads, and observability provisioning. It works across any Linux server with SSH access. It has zero dependencies on the server side. It is the deploy tool I wished I had across every project.

And it started as a 50-line bash script that I almost deleted because it was "not polished enough."

The takeaway

Not every tool needs a grand design. Some of the most useful software starts as the laziest possible solution to a personal pain point. The key is to ship it before you are ready, use it until you know what is missing, and let the real problems — not the imagined ones — drive what comes next.

If I had waited until I had a proper architecture, a test suite, and a documented API, PATTY CLI would not exist. It would still be a series of manual SSH sessions and bash scripts that I copied from project to project, each one slightly different, each one a potential footgun.

The best version of a tool is the one that exists. The second-best is the one that almost existed because you were waiting for the perfect design. Ship the ugly version. Use it. Fix what hurts. Repeat.