* 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. |
||
|---|---|---|
| .. | ||
| README.md | ||
| setup_backup_schedule | ||
| zbackup | ||
| zbackup_and_sync | ||
| zbackup_ec2 | ||
| zconfig.example.json | ||
| zdeploy | ||
| zec2 | ||
| zec2online | ||
| zhelpers.sh | ||
| zkill | ||
| zrepair | ||
| zrestart | ||
| zsetup | ||
| zsetup_mail | ||
| zstart | ||
| zstart_docker | ||
| zstop | ||
| zsync | ||
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(forzkill/zrestart),rsync(forzsync <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:
$ZTOKENS_DATAztokens.dataDirinzconfig.json- the sibling
../ztokens/datadirectory, if present ~/.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>mirrorsdist/withrsync -a --delete(robocopy /MIR equivalent).- Scheduling uses cron (
setup_backup_schedule) instead of Windows Task Scheduler. - The MOTD banner picks a random
motd/*.txtinstead of tracking a shuffle rotation. - Windows-only helpers (
.cmdlaunchers) don't exist — scripts are directly executable. - A leading dash on a project key is only tolerated by
zdeploy. PowerShell accepts-myappanywhere; in bash every other script treats-myappas an unknown option and exits 1. Tracked as a parity gap — use bare keys in the bash port.