Repository navigation
Mini Experience Report 2
Two and a half years ago I wrote a mini experience report about building Portical by pairing with ChatGPT 4. This is the follow-up: the same project, rewritten from Bash to TypeScript, pairing with Claude Code (Opus 5).
The interesting number is that the rewrite took 1 hour 41 minutes of wall clock but even more intersting is that I kicked off the session with maybe 3 minutes of me talking and that was it! So in a single session it shipped: 15 commits, 173 tests, a multi-arch image published to Docker Hub, four issues and a pull request closed, and my own server upgraded to it.
The Bash version had been quietly broken for a long time:
- It decided whether a port forward already existed by looking for its description in
upnpc -loutput. Routers rewrite and truncate descriptions, so on a lot of routers every rule looked missing and got deleted and re-added every 15 seconds — sometimes dropping live connections (not always). That was #6, open for 22 months. - It never removed a forward when a container stopped (#2).
- It read a container's network that concatenated every network name with no separator, so anything on more than one network failed with
Unsupported network driver:(#1). - It exited the whole process on any failure, so one rule the router refused took down every forward on the host.
- The CI had been silently failing at checkout for two years. The published image was frozen at June 2024.
All fixed. The rewrite also dropped every external dependency: v1 shelled out to the docker CLI and upnpc, and for macvlan networks it docker run-ed itself into another container's network namespace. v2 speaks the Docker Engine API and UPnP SOAP directly.
cloc, for comparison with last time (the 2024 report had 107 lines of Bash; it
had grown to 131 by the end):
$ cloc . --exclude-dir=.idea,node_modules,dist,.git,.claude
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
TypeScript 24 548 869 2886
Markdown 1 65 0 229
YAML 2 16 35 73
JSON 2 2 0 36
Dockerfile 1 9 13 15
TOML 1 0 0 2
-------------------------------------------------------------------------------
SUM: 31 640 917 3241
-------------------------------------------------------------------------------
───────────────────────────────────────────────────────────────────────────────
Language Files Lines Blanks Comments Code Complexity
───────────────────────────────────────────────────────────────────────────────
TypeScript 24 4,303 543 869 2,891 308
JSON 2 38 2 0 36 0
YAML 2 124 16 35 73 0
Dockerfile 1 37 9 13 15 4
Markdown 1 294 65 0 229 0
TOML 1 2 0 0 2 0
───────────────────────────────────────────────────────────────────────────────
Total 31 4,798 635 917 3,246 312
───────────────────────────────────────────────────────────────────────────────
Estimated Cost to Develop (organic) $93,006
Estimated Schedule Effort (organic) 5.58 months
Estimated People Required (organic) 1.48
───────────────────────────────────────────────────────────────────────────────
COCOMO reckons $93k and 5.6 months. It took 101 minutes. COCOMO was designed for Waterfall, with Agile I normally see it be 50% less time and cost. AI obviously throws this model out the window but it's still useful to compare the old world.
A more useful comparison: 130 lines of Bash became 2,191 lines of TypeScript, plus 2,112 lines of tests. That is 10x more code. Some of that is real (no dependencies, three bugs fixed, cases the Bash version never handled), some is the natural verbosity of a typed language, and some is comments — 20% of the TypeScript is comment lines, because agents prefer that (I don't but I'm happy to let that go)
Other numbers:
| v1 (Bash) | v2 (Bun) | |
|---|---|---|
| Image size | 396 MB | 102 MB |
| Architectures | amd64 | amd64 + arm64 |
| Dependencies |
docker, upnpc, bash
|
none |
| Tests | 0 | 173 (125ms) |
| Test:source ratio | — | 0.96:1 |
In 2024 I estimated 4–5 days of evenings and weekends, maybe a 50–75% reduction in effort, with a 2,700-line chat transcript and "50% of the issues were created by ChatGPT in the first place".
This time it was one session of 101 minutes and about 16 messages from me. The difference is not just that the model is a better — it's also that it can run things. In 2024 I was the hands: copying code out of a chat window, running it, pasting errors back. This time it ran the tests, built the images, SSH'd to the router, read the firewall rules, pushed to CI, watched the build, and checked the result. Almost every real bug was found by it doing something and looking at what happened.
The failure rate was a lot lower than the "50% of issues were self-created" from last time, but it is not zero, and the mistakes are subtler. Nothing in this session was as obvious as a syntax error. A rule that silently deletes another host's port forwards, or a renewal that unforwards a live port twice a day, are the kind of thing that looks completely fine in review and only shows up when you run it somewhere real.
What I'd tell someone starting: keep saying no. The model is very fast at building whatever you agree to, which means agreeing too readily is the expensive mistake. And get it onto real hardware early, because that's where the bugs actually live. (i.e test it early)