Skip to content

feat(scripts): manage personal CLI scripts via bin/ and symlinks - #11

Open
jiunbae wants to merge 5 commits into
masterfrom
feat/personal-scripts
Open

feat(scripts): manage personal CLI scripts via bin/ and symlinks#11
jiunbae wants to merge 5 commits into
masterfrom
feat/personal-scripts

Conversation

@jiunbae

@jiunbae jiunbae commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a scripts component that links bin/ into ~/.local/bin via backup_and_link
  • rewrite subd (v2): direct argv exec, exit-code propagation, --parallel, --only-git, --timeout, --dry-run
  • fix the migrated scripts for PATH and macOS before publishing them
  • keep work-specific scripts out of this repo since it is public

Why subd was rewritten

v1 ran every command through $SHELL -i, which cost ~880ms of rc/prompt startup per directory. The prompt noise that produced was stripped by a broad sed filter that also deleted real output — a line like [2026-08-24 ERROR] db connection failed never reached the terminal. It flattened the command into a string (${COMMAND[*]}), so quoting and globs were corrupted, and it always exited 0 no matter what the child commands did.

v2 execs argv directly with no shell in between and keeps -i as an opt-in for alias expansion. --depth is validated (-d abc previously ran nothing and reported success), failures propagate to the exit code, and colour is TTY-aware.

On a 26-directory tree, git status --short goes from 30.0s to 0.24s, or 0.05s with --only-git --parallel. Output is byte-identical to v1 once the prompt noise is removed.

Portability

set -u makes bash 3.2 and 4.3 treat an empty array expansion as an unbound variable, and subd's pre array is empty whenever --timeout is unset — every default invocation. macOS ships bash 3.2 and the scripts component is part of --all, so subd died on the first directory of every macOS install. Possibly-empty arrays now expand as ${arr[@]+"${arr[@]}"}, and wait -n (bash 4.3+) falls back to batch waiting. Verified on 3.2.57, 4.3.48, 4.4.23 and 5.0.18.

Review pass

A review of the branch turned up several things worth calling out, all fixed here:

  • install_scripts returned 1 when bin/ was missing; under install.sh's set -euo pipefail and ERR trap that aborted the whole run, so ./install.sh scripts ssh exited 1 with ssh never reaching its module. modules/ssh.sh:76 already handles the same case with log_warn + return 0.
  • bundle.sh embedded install.sh, lib/, modules/ and configs/ but not bin/ — the bundled installer shipped modules/scripts.sh with nothing to link, which is the failure its own comment warns about.
  • The idempotence check only recognised symlinks, so under --copy (which bundle.sh always passes) every re-run backed up and rewrote all of bin/, leaving executable .backup.<ts> files in a PATH directory.
  • Dropping a script from bin/ left its symlink behind, dangling on PATH; re-running the component now prunes those instead of accumulating them.
  • In subd, the gitstatus branch of the -i noise filter had no anchor, so any line containing that substring was dropped — the same silent-deletion defect v2 was meant to end. Child commands also inherited subd's own stdin, so the first directory drained it and the rest read nothing, differing from parallel mode; and "no matching directories" returned 0, reporting success for a run that executed nothing (now 3, as grep does).

Hardening the migrated scripts

The scripts came over from ~/.scripts unchanged. Linking them onto PATH under a component that also runs on macOS made their existing gaps reachable:

  • mkln built relative links with realpath --relative-to, a GNU coreutils flag macOS lacks, and reported success while producing empty link targets. An empty source directory also left a link named *.
  • machine-info assumed lscpu, free and nvidia-smi all exist, failed its core arithmetic without lscpu, printed an empty GPU: () without nvidia-smi, and mangled every GPU model after the first on heterogeneous hosts.
  • pip-install.sh never validated its argument, so a bare invocation expanded "$TARGET_DIR"/* to /* and ran pip install -e against every top-level directory. It also discarded pip's diagnostics and always exited 0.
  • get_audio_duration.sh emitted a CSV row for the unexpanded glob when a directory held no .wav.
  • mp4-to-wav.sh exited 0 after failing every conversion.

Scope

bin/ holds seven general-purpose scripts. The callabo and rt-* tooling stays out, along with install.sh (wired to internal NFS and /opt/tools paths, and destructive under --overwrite), ssh-list (keyed to office/IDC host groups), and slack/ntfy (carry endpoint values).

~/.local/bin precedes ~/.scripts on PATH, so these links override the copy shared over NFS without deleting it; removing a link rolls back to it.

🤖 Generated with Claude Code

jiunbae and others added 5 commits August 24, 2026 18:54
Add a `scripts` component that links the scripts in bin/ into
~/.local/bin. That directory precedes ~/.scripts on PATH, so the repo
copy overrides an older one (e.g. shared over NFS) without deleting it,
and removing a link rolls straight back to that copy.

Rewrite subd (v2). It ran every command through an interactive shell,
which cost ~880ms of rc/prompt startup per directory and emitted noise
that a broad sed filter then stripped — along with real output such as
`[2026-08-24 ERROR] db connection failed`. It also flattened the command
into a string, so quoting and globs were corrupted, and it always exited
0 regardless of what the child commands did.

v2 execs argv directly with no shell in between, keeps -i as an opt-in
for alias expansion, propagates a non-zero exit code, and validates
--depth (`-d abc` silently ran nothing before). Adds --parallel/-j,
--only-git, --timeout, --dry-run, -x, --prune, and TTY-aware color.
On a 26-directory tree, `git status --short` goes from 30.0s to 0.24s,
or 0.05s with --only-git --parallel; output is byte-identical to v1
once the prompt noise is removed.

Work scripts stay out of this repo since it is public: the callabo and
rt-* tooling, a bootstrap script wired to internal NFS and /opt/tools
paths, and an ssh helper keyed to office/IDC host groups.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`set -u` made bash 3.2 and 4.3 treat an empty array expansion as an
unbound variable, and `pre` is empty whenever --timeout is unset — that
is every default invocation. subd died on the first directory with
`pre[@]: unbound variable`, then again on `kids[@]` while walking the
tree. macOS ships bash 3.2, and the `scripts` component is part of
--all, so this broke every macOS install.

Expand possibly-empty arrays as ${arr[@]+"${arr[@]}"}, which is safe on
every version, and fall back to batch waiting where `wait -n` (bash
4.3+) is unavailable.

Verified on 3.2.57, 4.3.48, 4.4.23 and 5.0.18: sequential and parallel
runs both complete with no unbound-variable errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropping a script from bin/ left its symlink behind in ~/.local/bin,
pointing at a path that no longer exists — a dangling entry on PATH that
no later run cleaned up. Removing rt-*, install.sh and ssh-list from
bin/ during this branch's own review required deleting five links by
hand. Remove links that point into our bin/ and no longer resolve, so
re-running the component converges instead of accumulating.

bin/ is a PATH directory, so a file without the execute bit would link
in as an unrunnable command. Skip those with a warning naming the fix
rather than linking them silently.

Report linked, current, skipped and pruned separately; the previous
counter conflated "already current" with "skipped".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
install_scripts returned 1 when bin/ was missing, and install.sh runs
under `set -euo pipefail` with an ERR trap, so one absent directory
aborted the whole run — `./install.sh scripts ssh` exited 1 with ssh
never reaching its module. modules/ssh.sh:76 already handles the same
case with log_warn and return 0; match it.

bundle.sh embedded install.sh, lib/, modules/ and configs/ but not
bin/, so the bundled installer shipped modules/scripts.sh with nothing
for it to link — the exact failure its own comment warns about. Embed
bin/ alongside the other trees.

The idempotence check only recognised symlinks, so under --copy, which
bundle.sh always passes, every re-run backed up and rewrote all of bin/
and left executable .backup.<ts> files in a PATH directory. Compare
content in copy mode instead.

In subd, the `gitstatus` branch of NOISE_RE had no anchor, so under -i
any line containing that substring was dropped — `git log` output
mentioning gitstatus vanished, which is the defect v2 was meant to end.
Anchor it to the p10k banner. Child commands also inherited subd's own
stdin, so in sequential mode the first directory drained it and the rest
read nothing, differing from parallel mode where bash redirects async
commands from /dev/null; give every child /dev/null. And "no matching
directories" returned 0, reporting success for a run that executed
nothing: return 3, as grep does for no matches.

pip-install.sh never validated its argument, so a bare invocation
expanded "$TARGET_DIR"/* to /* and ran pip install -e against every
top-level directory. It also discarded all pip diagnostics, always
exited 0, and used a regex membership test where a literal one was
meant. It is now on PATH under its own name, so guard the argument,
let pip's output through, and propagate failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These came over from ~/.scripts unchanged. Linking them onto PATH under
a component that also runs on macOS made their existing gaps reachable,
so fix them here rather than shipping them as-is.

mkln built relative links with `realpath --relative-to`, a GNU coreutils
flag macOS does not have — there, realpath is absent from the base
system and the BSD build rejects the flag, so every link target came out
empty while the script still reported success. Compute the relative path
in shell instead. An empty source directory also left a link literally
named `*`, since the unmatched glob was used as a filename; `ln -s`
failures were ignored the same way. Guard with nullglob, count failures,
and drop the unused absolute_source_dir.

machine-info assumed lscpu, free and nvidia-smi all exist. Without lscpu
the core arithmetic failed outright; without nvidia-smi it printed the
empty `GPU: ()`; and its single-line awk reformatter mangled every GPU
model after the first on heterogeneous hosts. Read from sysctl/vm_stat
on Darwin, degrade to "unknown"/"unavailable"/"none detected" when a
tool is missing, and print one line per GPU model. Core count now
multiplies by socket count, which the old expression omitted.

get_audio_duration.sh emitted a data row for the unexpanded glob when a
directory held no .wav, folding a phantom file into the CSV a consumer
reads; convert_sample_rate.sh next door already guards this. mp4-to-wav.sh
exited 0 after failing every conversion, so `mp4-to-wav.sh dir && train`
proceeded with no WAVs.

In subd, trim_blank_edges forked awk per directory even though `$(...)`
has already stripped trailing newlines; skip it when the output has no
whitespace edge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant