The End of Script Maintenance Hell. Introducing typerx.

There is a particular kind of suffering that every developer knows.

You wrote a bash script six months ago. It worked. You were proud of it. Then you came back to it on a Sunday at 11pm because something in production broke. You stared at it. You could not remember what ${2:-} does. You could not tell which function calls which. You added a set -e in the wrong place. You spent two hours on something that should have taken five minutes.

That is script maintenance hell. And it is completely avoidable.


The Problem With Bash

Bash is brilliant for glue. Ten lines of pipe magic. A quick cron job. A deployment one-liner.

But when your script grows past 200 lines — when it has argument parsing, multiple subcommands, error handling, coloured output, env file manipulation, YAML patching, and API calls — bash becomes a liability.

Not because bash is bad. Because bash was never designed for that job.

You end up with:

  • Argument parsing that takes 50 lines and still misses edge cases
  • Colour codes copy-pasted from Stack Overflow
  • read_field() functions that use heredocs to embed Python inside bash
  • Zero IDE support, no autocompletion, no type hints
  • Tests that nobody writes because testing bash is painful
  • Maintenance that falls apart the moment someone else touches the file

I know because I lived it. My setup-ga.sh script — a Google Analytics setup tool for my Hyperdrift workspace — grew to 800 lines of this.

It worked. It was also a maze.


The Approach: Typer + Rich, done right

Python's Typer library, built by Sebastián Ramírez (the FastAPI author), is the cleanest solution I have found for CLI scripts. Pair it with Rich for terminal output and you get something that feels like a proper application — with almost no boilerplate.

The problem is that every project reinvents the same utilities:

  • Pretty logging (info, success, warn, error)
  • Reading and writing .env files
  • Parsing apps.yml for app metadata
  • Syncing secrets to Ansible vault
  • Running gcloud or gh commands
  • Deploying apps and checking launch readiness

You write these once per project, then copy-paste them into the next one, then forget which version is current, then find three slightly different implementations across five repos.

typerx is the solution: a shared library of all of these utilities, designed to be the batteries-included foundation for any real-world devops script.


What typerx gives you

from lib import typerx as tx

tx.info("Reading config...")
tx.success("Property created!")
tx.warn("Don't forget to sync vault")
tx.error("App not found in apps.yml")
tx.step("Deploying to production")
tx.uri("https://hyperdrift.io")

That is your logging — done. No colour code strings. No repeated echo -e "${GREEN}✓${NC}".

# Read from apps.yml
base_url = tx.read_app_field("intel", "base_url")
port     = tx.read_dev_port("intel")

# Read/write .env files
measurement_id = tx.env_get(Path(".env.local"), "GA_MEASUREMENT_ID")
tx.env_set(Path(".env.local"), "GA_MEASUREMENT_ID", "G-XXXXXXX")

# Sync to Ansible vault
tx.vault_sync(env_file, vault_name)

# Deploy and validate
tx.deploy_app("intel")
tx.check_launch_readiness("intel")

And the CLI structure with Typer:

app = typer.Typer(no_args_is_help=True)

@app.command()
def create(
    app_name: str = typer.Argument(..., help="App name from apps.yml"),
    account_id: Optional[str] = typer.Option(None, "--account-id", envvar="GA_ACCOUNT_ID"),
    key_file: Optional[str] = typer.Option(None, "--key-file"),
) -> None:
    """Create a GA4 property + web data stream and update apps.yml."""
    tx.step(f"Creating GA4 property for {app_name}")
    ...

if __name__ == "__main__":
    app()

You get --help for free. Tab completion for free. Type validation for free. Error messages that don't say "bad substitution".


The result: setup-ga.sh → setup-ga.py

Here is the before and after on my GA setup script.

Metricsetup-ga.shsetup-ga.py
Lines of code800299
Argument parsing60 lines of caseTyper decorators
Coloured outputCopy-pasted escape codestx.info(), tx.success()
YAML patchingInline Python heredoc inside bash_patch_apps_yml() function
--helpManually maintained usage()Auto-generated by Typer
IDE supportNoneFull (type hints, autocompletion)
TestabilityHardStandard pytest

The logic is identical. The implementation is 63% shorter and can be read by a human.


The architecture

scripts/
├── lib/
│   ├── typerx.py      ← The shared framework
│   ├── hd.py          ← Backward-compat shim (re-exports typerx)
│   └── pyproject.toml ← For PyPI publishing
├── setup-ga.py        ← GA4 management (was setup-ga.sh)
├── setup-oauth.py     ← OAuth setup (uses typerx)
└── ...

The Node.js runners for GA API calls are kept — they use the official @google-analytics/admin SDK, which is the right tool for that job. typerx's Python layer handles all the orchestration: credential resolution, apps.yml patching, npm install checking, vault sync instructions.

The two-layer approach is deliberate: use the right language for each concern, but centralise the DevOps glue in one well-maintained place.


Why this matters for productivity

The argument for well-structured scripts is the same as the argument for any well-structured code: the real cost is maintenance, not creation.

A 800-line bash script costs you 2 hours to write. It costs you 30 minutes every time you touch it for the next two years. Over 24 months, that is 600 minutes of friction on a script that should take 5 minutes to understand.

A 299-line Python file with Typer and typerx takes a bit longer to write. It costs you 5 minutes to understand, every time, forever. The IDE tells you the types. The tests tell you if something broke. The --help tells anyone else what the commands do.

That is not over-engineering. That is the long-term thinking that separates maintainable systems from maintenance hell.


Try it

typerx is at github.com/hyperdrift-io/typerx. It is designed to be either installed or dropped in as a single file — no internal dependencies beyond Typer and Rich.

# Install via pip
pip install typerx

# Or copy the single file into your project
curl -fsSL https://raw.githubusercontent.com/hyperdrift-io/typerx/main/typerx.py \
  -o scripts/lib/typerx.py

# In your script
from lib import typerx as tx
app = typer.Typer()

@app.command()
def deploy(app_name: str) -> None:
    tx.step(f"Deploying {app_name}")
    tx.deploy_app(app_name)

The end of maintenance hell is not a different tool. It is the same tools, used with discipline.


typerx is part of the Hyperdrift open toolchain — a collection of scripts, patterns, and infrastructure for building and shipping software products without the overhead.

Next in this series: One command. Any machine. The story behind nvim-config.

Get weekly intel — courtesy of intel.hyperdrift.io