refactor: replace crates/ with zsh scripts, add zarg - #82
Merged
Conversation
15 Rust binaries (3097 lines, 326 transitive crates) become 15 zsh scripts in scripts/, sharing output helpers in scripts/lib/common.zsh. Ten of the fifteen shelled out to ffmpeg/metaflac/aria2c/unzip/beet/exiftool anyway; the Rust was a progress bar around someone else's program. Drops the C toolchain from Linux bootstrap (git2 vendored libgit2) and the rust toolchain from mise. task reinstall-bins had not rebuilt anything since its first run — its status guard was satisfied forever after. zsh-plugins/zarg derives parsing, --help, --version and completions for zsh/fish/bash from one declaration, so a script's completions cannot drift from the flags it accepts. 32 tests, plus scripts/lib/check-completions asserting every emitted completion parses in its target shell. Three bugs fixed in the port: extract-exif-from-flac could never report "Clean" (substring-matched the sensitive list against JSON containing "SourceFile"); prune-gen's sparse fixtures made a 210MB dir read as 16KB; the warning glyph was an empty string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014CzNvwMVqrpyy2N1vhHQui
scripts/README.md documents each command with worked examples, and zarg's README gains a section on using it from other plugins. wtclean was already a script rather than a function, so it drops its hand-rolled -n check for a zarg spec and gains --help, --version and its first completion. The rest of wt-* stays hand-parsed: wt and wtrm cd, so they must remain functions, and zarg_go calls exit — which in an interactive function kills the shell, not the command. Reaching wtclean goes through a shell function, so `command -v` finds nothing from inside a task. Added generate:completions:plugin, which keys off an executable path instead. check-completions matched the new README (it documents zarg.plugin.zsh) and tried to execute it. Now gated on the executable bit and a zarg_go call, and its default sweep covers plugin bin/ dirs so wtclean is checked in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014CzNvwMVqrpyy2N1vhHQui
scripts/README.md landed in a glob that assumed everything under scripts/ was a script. Gate on the executable bit, or a .zsh extension for sourced files, and pick up plugin bin/ dirs while here so wtclean is covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014CzNvwMVqrpyy2N1vhHQui
This was referenced Aug 7, 2026
radiosilence
added a commit
that referenced
this pull request
Aug 7, 2026
wtclean moved to zarg in #82 and the other four were left hand-rolled, which is exactly the drift zarg exists to prevent. One declaration now yields the parser, --help and completions for all five. Branch arguments reuse _wt_branches, the function already backing the wt and wtrm compdefs, rather than a second way to list worktrees. Claude-Session: https://claude.ai/code/session_01SdK2s5sJBVLc5Edhm3cZ3H Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
radiosilence
added a commit
that referenced
this pull request
Aug 7, 2026
* refactor(wt): parse the remaining bins with zarg wtclean moved to zarg in #82 and the other four were left hand-rolled, which is exactly the drift zarg exists to prevent. One declaration now yields the parser, --help and completions for all five. Branch arguments reuse _wt_branches, the function already backing the wt and wtrm compdefs, rather than a second way to list worktrees. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdK2s5sJBVLc5Edhm3cZ3H * refactor(wt): move wtclean's logic into the script The plugin defined _wt_clean, _wt_pr_cache and _wt_fan, and bin/wtclean was their only caller — every interactive shell parsed ~150 lines it would never run. Worse, zarg parsed --dry-run into $dry_run and the script re-encoded it back into -n so the function could parse it again, with a comment explaining the gotcha that round trip created. The genuinely shared helpers (_wt_root, _wt_base, _wt_named, _wt_pr_state) stay in the plugin, where wtrm and the picker use them. _wt_prs keeps its declaration next to _wt_pr_state rather than moving with the code that fills it: an undeclared associative array makes ${_wt_prs[feat/x]} an arithmetic subscript, so "feat/x" is evaluated as a division and any slashed branch name dies with "division by zero". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdK2s5sJBVLc5Edhm3cZ3H * docs(wt): record the wtclean split and the _wt_prs subscript trap Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdK2s5sJBVLc5Edhm3cZ3H --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Deletes
crates/— 15 Rust binaries, 3097 lines, 326 transitive crates — and replaces them with 15 zsh scripts inscripts/, plus azargplugin that gives them all argument parsing and completions. Net -4600 lines.Why
Ten of the fifteen binaries shelled out to
ffmpeg,metaflac,aria2c,unzip,beetorexiftoolanyway, so the Rust was a progress bar wrapped around someone else's program. The costs were real:git2vendored libgit2, which is the sole reasonsetup-linuxinstalledbuild-essential;reqwest+ rustls came along for two HTTP GETscurlalready does.clean-exifwas the only binary doing real in-process work and it still loses toexiftool -all= -overwrite_original, which strips XMP/IPTC/ICC as well.Every replacement tool was already installed. The only additions are
lsofandlibimage-exiftool-perlon Linux, covering the two thingskill-portandclean-exifused to do in-process.task reinstall-binshad not rebuilt anything in a long time. Its guard wasstatus: ["test -f {{.DOTFILES}}/bin/clean-dls"]— satisfied forever after the first install, so later edits tocrates/were skipped silently and the cheatsheet's "updrebuilds rust bins" was false.zarg: declare once, derive everything
Each script needs a parser and a compdef, and hand-written pairs drift the moment a flag is added.
zsh-plugins/zargtakes one declaration and derives parsing,--help,--versionand completions for three shells from it.The whole of
scripts/kill-port's interface:That alone produces
--help:…typo correction:
…and
--completions zsh:…
--completions fish:…and
--completions bash:Parsing covers
--long value,--long=value,-s value, attached shorts (-b192), clustered flags (-nk),--, defaults,env=fallbacks,required,variadicand validatedvalues=sets.Completion fidelity differs by shell and that's the shells' doing:
complete=names a zsh function, so fish and bash fall back to file completion there. Declared value sets work in all three.Using the scripts
Full reference with examples in
scripts/README.md. A sampler:pruneskips dotted directories and their subtrees entirely, so.gitand.stfolderare never candidates, and collapses nested candidates to their topmost parent.vimvrefuses if the line count changes, because a shifted list renames every file to its neighbour's name.wtclean uses zarg too
It was already a script rather than a function — a GC pass has no reason to mutate the calling shell — so it drops its hand-rolled
[[ $1 == -n ]]and gains--help,--version, typo correction and the first completion it has ever had.The rest of
wt-*deliberately doesn't.wtandwtrmcd, so they must stay functions, and zarg is wrong for functions on two counts:zarg_gocallsexit, which in an interactive function kills the user's shell rather than the command, andtypeset -gwould leak parsed values and theZARG_*spec arrays into the session. They keepcompdefat load time.That needed a
generate:completions:plugintask —wtcleanis reached through a shell function, socommand -v wtcleanfinds nothing from inside a task and the usual generator skipped it silently.Deliberate behaviour changes
to-audio flac|opusis a positional, not a subcommand--bitrate, meaningless for lossless). Now ignored for flac rather than absentprunemeasures blocks (du), not apparent sizekill-port -stakes names only-s 9now errors pointing atKILLrather than being silently accepted""— an empty string, so warnings rendered with colour and no iconBugs fixed in the port
extract-exif-from-flaccould never report "Clean". It substring-matched the sensitive-field list against the wholeexiftool -jsondocument, and exiftool always emits"SourceFile"— which containsSource. Every image came back dirty. Now asks exiftool for only the tags that matter, so an answer at all is the finding.prune-gen's fixtures were sparse, so a "210MB" directory read as 16KB underduand the fixture had stopped exercising the threshold it exists to test.(( x++ ))truthiness bugs: post-increment returns the old value, so(( ok++ )) || (( failed++ ))double-counted the first success as a failure, and(( failed )) && _dt_info ...as a last line made a clean run exit 1.local pathinprune— zsh ties$pathto$PATH, so it silently emptied PATH mid-run anddu/cut/sortstarted vanishing.How to verify
Both run in CI (
scripts.yml, replacingrust-tests.yml) and on pre-push. CI installs fish so the fish output is parsed by real fish rather than merely generated.Each script was exercised against fixtures, not just
--help:clean-dlsclassified 24 files correctly in both directions (including a filename containing a newline);to-audioround-tripped wav→flac→opus with spaces in filenames;clean-exifconfirmed to strip Artist/Copyright/UserComment and to count a corrupt file as failed;vimvconfirmed to usegit mvand to refuse a line-count mismatch with exit 1;git-syncagainst a real deleted upstream while keeping a no-upstream branch;git-squashcollapsing 2→1 with the tree intact;embed-art→metaflacpicture block landed;parallel-dl-extractagainst a real zip over HTTP;url2base64round-tripped throughbase64 -d.impran with stubbedaria2c/beet;unfuck-xcodeonly via--help/-n.Fallout
link:cargo+packager.d/cargo-config.tomldeleted,rustdropped from03-tools.toml,~/.dotfiles/binoff$PATH, cargo artefacts out of.gitignore,DOTFILES_NO_RUSTescape hatch gone.Still questionable
prune-genis a test fixture generator that ships as a command and gets a shell completion. It survived because it was in scope, not because it earns its place — worth deleting separately.🤖 Generated with Claude Code
https://claude.ai/code/session_014CzNvwMVqrpyy2N1vhHQui