Assisted-by: Codex
6.4 KiB
Implementation language: bash vs. Python vs. Go
Research into which runtime bot-bottle should be implemented in, given
where the project is today (~1250 lines, Python, mostly orchestration of
docker / flyctl / ssh). The project started in bash and was rewritten
to Python; this note evaluates whether either of the other two options
would be a better fit going forward.
Summary
Stay on Python. Switch to Go if and when distribution friction becomes the dominant pain — i.e., when bug reports about Python interpreter / venv behavior start outweighing bug reports about bot-bottle itself. Bash is not the right tool at the project's current size; reverting would be a regression.
The single thing worth doing now regardless of language is keeping the backend abstraction (local-docker / generic-remote / fly) clean enough that a future Go rewrite would be mechanical. If the abstraction is right, the language choice is reversible. If it isn't, the cost of switching balloons because you're rewriting and redesigning at once.
Axes that matter for this project
The relevant criteria, in roughly the order they bite:
- Distribution friction — how easy is it to install the tool on a new dev machine.
- Orchestration ergonomics — 90% of the work is shelling out to
docker,flyctl, andssh, so impedance match with subprocess invocation matters. - JSON manifest handling — the manifest is structured config with nested fields, validation rules, and per-agent overrides.
- Cross-platform behavior — must work the same on macOS and Linux, ideally without per-platform shims.
- Test-matrix burden — local-mac × local-linux × generic-remote × fly is already a wide test surface. The language choice should minimize what it adds to that matrix, not expand it.
- Onboarding new contributors — single maintainer today, but the project is published and may attract drive-by PRs.
Comparison
| Bash | Python | Go | |
|---|---|---|---|
| Distribution | Zero runtime — chmod +x && run |
uv run is good; bare python3 is "which one" |
Single static binary, go install |
| Orchestration ergonomics | Native — pipes, set -e, no marshaling |
Verbose (subprocess.run(...) × 50) |
Verbose (if err != nil { return err } × 50) |
| JSON manifest | Painful past trivial — jq for nested writes is ugly |
Excellent — json + dataclasses/pydantic |
Excellent — struct tags, compile-time schema |
| Cross-platform | macOS bash 3.2 vs Linux 5.x + BSD/GNU userland = real hazard | Mostly a non-issue with uv |
Best — same binary everywhere |
| Testability | Hard. No good unit story | Mature (pytest, subprocess mocking) | Mature (table-driven, exec.Command is mockable) |
| Startup time | ~5ms | ~100-200ms cold | ~10ms |
| Onboarding new contributors | High barrier past ~500 lines | Largest pool | Smaller but technical pool |
| Rewrite cost from current state | Already done once, regretted | Sunk cost — zero | ~1 focused week for ~1200 lines |
Bash
Right tool if the project stays under ~500 lines. bot-bottle has already crossed that threshold (~1250 lines), and the orchestration is no longer "stitch CLIs together" — it has manifest validation, env-var resolution, network and sidecar lifecycle, and SSH provisioning. Bash scales badly for all four:
- macOS bash 3.2 ceiling. No associative arrays, no
wait -n, no${var,,}, no namerefs. Anything written for Linux bash 5.x has to be back-ported by hand. - BSD vs GNU userland divergence.
sed -i,date,readlink,mktempflags all behave differently. Every script grows portability shims. - JSON via
jqis workable but ugly. Nested writes and per-agent override merges become unreadable. - No real test story. Black-box integration tests only.
- Silent failure modes.
set -euo pipefaildoes not catch every case (e.g.nullpropagating through a pipeline into adocker runflag), and command substitutions can loseset -e. Subtle, hard to audit.
The original project was written in bash and rewritten to Python for exactly these reasons. Reverting would not be a portability win; it would be a portability loss.
Python
Right tool for where the project is now. The sunk cost is real and
accurate: it's testable, the orchestration code reads cleanly, the
manifest layer benefits from structured types, and uv plus PEP 723
inline metadata makes the historical "which python3 with which deps"
complaint mostly historical.
Real costs that remain:
- Startup latency — ~100-200ms cold is noticeable but not bad for an
interactive tool that runs a single command and hands off to
docker exec -it. - Distribution to non-developer audiences — has rough edges if the
user doesn't have
uv. Acceptable for the current audience (developers who already have a Python). - Interpreter version drift — Ubuntu 22.04 ships 3.10, fresh distros ship 3.13. Behavior deltas between minor versions exist but are rare in the standard library surface this project uses.
Go
Right tool if and when distribution becomes the dominant pain. Single static binary works identically across macOS arm64, macOS amd64, Linux amd64, Linux arm64 — which neutralizes most of the cross-platform leg of the test matrix. Startup is fast enough that the tool feels native.
Costs:
- Rewrite cost — roughly one focused week for ~1200 lines of mostly mechanical orchestration. Not interesting work.
- Verbosity —
if err != nil { return err }is similar in volume to Python'ssubprocess.run(..., check=True)plumbing. No win on terseness. - Smaller AI-tooling ecosystem — most Claude Code-adjacent helpers and skills are Python or JS. Drive-by contributors are a smaller pool. Any future "import a third-party Python skill package" idea gets harder.
- Iteration loop — no "edit the script, rerun" — you build, then run. Minor; not load-bearing for a single maintainer.
Recommendation
Stay on Python. The signal to watch for, before reconsidering, is bug reports about Python interpreter or venv behavior outnumbering bug reports about bot-bottle's actual logic. Until that pattern shows up, the Go rewrite isn't paying for itself.
Independent of language: invest in the backend abstraction now. A clean
Backend interface (with run, exec, cp, build, network_*)
makes the language choice reversible. A leaky abstraction makes it
expensive in any direction.