zscripts-token-savers/bash
kellymichels 37a703ce19
test: bats suite for the bash port, + fix underscore-key guard (phase 5 of #26) (#32)
* test: add bats suite for the bash port + fix underscore-key guard (phase 5)

Part of #26. Closes #30.

The bash port reimplements the exclude lists, config accessors and argument
parsing, so it can drift from PowerShell independently. 50 bats tests mirror
the Pester suites assertion-for-assertion where the two are meant to agree:
z_archive_excludes (per-kind lists and the deploy-vs-backup gating), config
accessors, z_path Windows->WSL translation, json_build_label, and argument
handling (bare invocation, unknown key, 'all' expansion, --port override).

Fixes #30 along the way, because the alternative was a test enshrining the bug:
zproj_require accepted underscore comment keys. It only checked the key was
non-null, and a comment is a non-null JSON string, so 'zkill _note' sailed
through and exited 0 having done nothing - the silent-success failure mode.
Now rejects any _-prefixed key and requires the value to be a JSON object.
Both checks earn their place: the type check catches string comments, the
prefix rule catches an object-valued _template key that PowerShell refuses and
the type check alone would allow.

Documents #31 rather than fixing it: a leading dash on a project key works in
every PowerShell script but only in bash/zdeploy - the others reject -myapp as
an unknown option. Stripping it everywhere would make a mistyped flag resolve
as a project key, so the tests pin current behaviour and bash/README.md now
states the difference instead of the README's blanket claim.

Verified by mutation testing: all 9 mutations turn the suite red - removing the
python and vite backup gates, reintroducing #23 in bash, unfiltering underscore
keys in zproj_keys and zproj_require, breaking zremote_compose_dir fallback and
z_path translation, and removing zkill's all-expansion and no-args guard. Both
mutated files confirmed restored byte-for-byte.

An early run also caught a bug in the tests themselves: the membership helper
used 'grep -qx' (regex), so the needle '.env' matched 'venv' and several
'excludes .env' assertions were false passes. Now uses -qxF.

* fix(gitattributes): keep .bats files LF so 'bats tests/bash' works on a Windows checkout

The LF rule was scoped to 'bash/**', which does not match tests/bash/. With
core.autocrlf a Windows working copy got CRLF .bats files, and bats fails on
them - so the command the README documents would not run on the machine the
suite was written on without stripping \r first.

Adds tests/bash/** and *.bats to the same eol=lf rule and renormalises.
Verified by running 'bats tests/bash' with no sed preprocessing: 50/50.
2026-07-26 13:02:23 -05:00
..
README.md test: bats suite for the bash port, + fix underscore-key guard (phase 5 of #26) (#32) 2026-07-26 13:02:23 -05:00
setup_backup_schedule fix(bash): zbackup — real pg_dump error reporting + require an explicit target (all) (#9) 2026-07-25 15:17:12 -05:00
zbackup fix(bash): zbackup — real pg_dump error reporting + require an explicit target (all) (#9) 2026-07-25 15:17:12 -05:00
zbackup_and_sync fix(bash): zbackup — real pg_dump error reporting + require an explicit target (all) (#9) 2026-07-25 15:17:12 -05:00
zbackup_ec2 fix: zbackup_ec2/zec2/zec2online require an explicit target (all), matching zbackup (#11) 2026-07-25 15:21:18 -05:00
zconfig.example.json feat(zsetup): new command to create a project's venv + install deps (#19) 2026-07-26 12:51:44 -05:00
zdeploy fix(bash): make the port bash-3.2 / BSD-clean so it runs on stock macOS (#8) 2026-07-22 18:41:11 -05:00
zec2 fix: zbackup_ec2/zec2/zec2online require an explicit target (all), matching zbackup (#11) 2026-07-25 15:21:18 -05:00
zec2online fix: zbackup_ec2/zec2/zec2online require an explicit target (all), matching zbackup (#11) 2026-07-25 15:21:18 -05:00
zhelpers.sh test: bats suite for the bash port, + fix underscore-key guard (phase 5 of #26) (#32) 2026-07-26 13:02:23 -05:00
zkill feat(zkill): support 'all' to stop every project's dev server (#25) 2026-07-24 17:35:14 -05:00
zrepair fix(bash): make the port bash-3.2 / BSD-clean so it runs on stock macOS (#8) 2026-07-22 18:41:11 -05:00
zrestart feat(bash): native bash port of all z-scripts for Linux/macOS/WSL (#5) 2026-07-22 16:35:05 -05:00
zsetup feat(zsetup): new command to create a project's venv + install deps (#19) 2026-07-26 12:51:44 -05:00
zsetup_mail feat(bash): native bash port of all z-scripts for Linux/macOS/WSL (#5) 2026-07-22 16:35:05 -05:00
zstart feat(zstart): uvicorn/startApp support + missing-venv warning + fix --detached hang (#12) 2026-07-25 22:17:14 -05:00
zstart_docker feat(bash): native bash port of all z-scripts for Linux/macOS/WSL (#5) 2026-07-22 16:35:05 -05:00
zstop feat(bash): native bash port of all z-scripts for Linux/macOS/WSL (#5) 2026-07-22 16:35:05 -05:00
zsync fix(bash): translate Windows config paths to WSL/Unix form so the bash port works on WSL (#7) 2026-07-22 18:40:52 -05:00

Token Savers — bash port (Linux / macOS / WSL)

Native bash versions of the z-scripts. Same commands, same zconfig.json schema, same behavior — no PowerShell required.

Requirements

  • bash 3.2+ (the stock macOS bash works), jq, curl, zip, OpenSSH (ssh/scp)
  • lsof (for zkill/zrestart), rsync (for zsync <project> mirror mode)
  • Docker + docker compose on the remote host for the deploy scripts

Debian/Ubuntu: sudo apt install jq curl zip lsof rsync macOS: brew install jq (bash, curl, zip, ssh, lsof and rsync already ship with macOS)

Install

git clone https://github.com/kellymichels/zscripts-token-savers.git ~/zscripts
cd ~/zscripts/bash
cp zconfig.example.json zconfig.json   # then fill in your values
chmod +x z* setup_backup_schedule
echo 'export PATH="$HOME/zscripts/bash:$PATH"' >> ~/.bashrc && source ~/.bashrc

Commands

Same set as the PowerShell versions — the project key in zconfig.json IS the command argument:

Command What it does
zsetup <p> create the project's python venv + install deps (or npm install)
zstart <p> [--detached] start local dev server (python/vite/nextjs)
zkill <p> free the project's dev port
zrestart <p> [--detached] kill + start in one command
zstop <p> stop the project's compose stack on the server
zdeploy <p> [--note "msg"] zip → scp → docker compose build/up → verify build version
zec2 [<p>] TCP + HTTP + live-version reachability check
zec2online [<p>] deep health check; auto-starts downed stacks
zrepair <p> audit/repair container + proxy routing, smoke test
zbackup [<p>] [--tag t] local source zip (+ pg_dump when .env has DATABASE_URL)
zbackup_ec2 [<p>] pull db dump + data dirs down from the server
zsync copy new backup files offsite (never overwrites)
zbackup_and_sync both of the above; cron-friendly
setup_backup_schedule --install daily backup cron job
zsetup_mail --domain d provision docker-mailserver mailboxes + print DNS records
zstart_docker bring up the local compose stack in bash/docker/

Flags use GNU style (--port 3000, --detached) instead of PowerShell style (-Port, -Detached). Detached dev servers log to /tmp/zstart-<project>.log.

Usage tracking

Every run prints a footer with its own output volume:

--- 1,048 lines / 69,009 chars / ~19,717 tokens est. (Claude Code) ---

and (when jq is available) appends one JSON line per run to tokens.jsonl, so you can see how much infrastructure output you keep out of an AI agent's context over time. Fields: ts, script, projects, lines, chars, est, model (est ≈ chars / 3.5). Nested runs (the zkill/zstart inside zrestart) are counted once, not double.

Where it's written, first match wins:

  1. $ZTOKENS_DATA
  2. ztokens.dataDir in zconfig.json
  3. the sibling ../ztokens/data directory, if present
  4. ~/.ztokens/data (created on first run)

Set ZTOKENS_MODEL to tag records with a specific model; it defaults to est. chars/3.5.

Tests

The bash port has its own bats suite, mirroring the PowerShell Pester tests assertion-for-assertion where the two are meant to agree — the exclude lists, config accessors, z_path translation, and argument handling:

bats tests/bash

Install bats without root:

git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core
/tmp/bats-core/install.sh ~/.local        # then ensure ~/.local/bin is on PATH

A fixture config is injected through ZCONFIG, so the suite never reads your real zconfig.json and passes on a machine that has never been configured. Tests that run the scripts stay on paths that exit before doing any work; only zkill runs with a real target, against deliberately unused ports.

Why a separate suite: the bash port reimplements the exclude lists and argument parsing, so it can drift from the PowerShell side independently. Both suites assert the same deploy-vs-backup rule — deploys drop .env* and uploads/, backups keep them — because that one has broken in production.

Differences from the PowerShell versions

  • zsync <viteproject> mirrors dist/ with rsync -a --delete (robocopy /MIR equivalent).
  • Scheduling uses cron (setup_backup_schedule) instead of Windows Task Scheduler.
  • The MOTD banner picks a random motd/*.txt instead of tracking a shuffle rotation.
  • Windows-only helpers (.cmd launchers) don't exist — scripts are directly executable.
  • A leading dash on a project key is only tolerated by zdeploy. PowerShell accepts -myapp anywhere; in bash every other script treats -myapp as an unknown option and exits 1. Tracked as a parity gap — use bare keys in the bash port.