From 239de23ccc0384b90e86a572bd02629180136aa9 Mon Sep 17 00:00:00 2001 From: Brandon Shoop Date: Tue, 2 Jun 2026 11:16:56 -0400 Subject: [PATCH 1/2] feat: add convert-video, shared bootstrap, common lib, fix sast brew bug - Add bin/_bootstrap: single location for symlink-crawl and CLI_TOOL_ROOT export - All bin scripts source _bootstrap instead of inlining the while loop - Extract gecho/show_progress/color vars into lib/common.sh - Add bin/convert-video and lib/ffmpegcmds.sh for H.264/AAC MP4 conversion - Fix sast defaulting PLUGINS_DIR to script install path (broke brew installs) - Add tests for _bootstrap and common.sh; fix test_gitcmds and test_sast for new structure - Update README structure section and add convert-video docs Closes #9 Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 +++- README.md | 31 ++++++++++++++++++++++--------- bin/_bootstrap | 10 ++++++++++ bin/convert-video | 7 +++++++ bin/gitprune | 10 +++------- bin/gitrefresh | 10 +++------- bin/sast | 8 +++++--- lib/common.sh | 13 +++++++++++++ lib/ffmpegcmds.sh | 36 ++++++++++++++++++++++++++++++++++++ lib/gitcmds.sh | 15 ++------------- tests/test_bootstrap.bats | 25 +++++++++++++++++++++++++ tests/test_common.bats | 34 ++++++++++++++++++++++++++++++++++ tests/test_gitcmds.bats | 1 + tests/test_sast.bats | 25 ++++++++++++++----------- 14 files changed, 178 insertions(+), 51 deletions(-) create mode 100644 bin/_bootstrap create mode 100644 bin/convert-video create mode 100644 lib/common.sh create mode 100644 lib/ffmpegcmds.sh create mode 100644 tests/test_bootstrap.bats create mode 100644 tests/test_common.bats diff --git a/.gitignore b/.gitignore index b060ff8..9571e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,6 @@ build/ !.env.example -change-summary.md \ No newline at end of file +change-summary.md +wiki/** +!.gitkeep \ No newline at end of file diff --git a/README.md b/README.md index 46408d6..c43f11c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # cli-tools -Personal shell utilities for git branch hygiene, GPG cache warming, and Claude plugin static analysis. +Personal shell utilities for git branch hygiene, GPG cache warming, video conversion, and Claude plugin static analysis. ## Installation @@ -52,9 +52,17 @@ Warms the GPG agent cache by performing a throwaway clearsign. Useful to pre-unl cache-gpg ``` -### `sast` +### `convert-video [file]` -Static analysis for Claude plugin markdown files. Scans `plugins/` for risky `allowed-tools` declarations in YAML frontmatter. +Converts a video file to H.264/AAC MP4 using ffmpeg. Output is written alongside the input with a `.mp4` extension. Requires `ffmpeg`. + +```sh +convert-video input.mov +``` + +### `sast [plugins-dir]` + +Static analysis for Claude plugin markdown files. Scans `plugins/` (default: `./plugins`) for risky `allowed-tools` declarations in YAML frontmatter. | Severity | Check | |----------|-------| @@ -63,7 +71,8 @@ Static analysis for Claude plugin markdown files. Scans `plugins/` for risky `al | WARN | Bare `WebFetch` — any domain fetchable | ```sh -sast +sast # scans ./plugins +sast /path/to/plugins # scans a specific directory ``` Exits non-zero if any ERROR findings are found. @@ -72,12 +81,16 @@ Exits non-zero if any ERROR findings are found. ``` bin/ - cache-gpg # warms GPG agent cache - gitprune # wrapper for gitprune() - gitrefresh # wrapper for gitrefresh() - sast # static analysis for claude plugin frontmatter + _bootstrap # resolves CLI_TOOL_ROOT; sourced by all bin scripts + cache-gpg # warms GPG agent cache + convert-video # wrapper for convert_vid() + gitprune # wrapper for gitprune() + gitrefresh # wrapper for gitrefresh() + sast # static analysis for claude plugin frontmatter lib/ - gitcmds.sh # shared function definitions + common.sh # shared color vars, gecho, show_progress + ffmpegcmds.sh # convert_vid() implementation + gitcmds.sh # gitprune() and gitrefresh() implementations ``` ## License diff --git a/bin/_bootstrap b/bin/_bootstrap new file mode 100644 index 0000000..fd4836f --- /dev/null +++ b/bin/_bootstrap @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +_src="${BASH_SOURCE[0]}" +while [ -L "$_src" ]; do + _dir="$(cd -P "$(dirname "$_src")" && pwd)" + _src="$(readlink "$_src")" + [[ "$_src" != /* ]] && _src="$_dir/$_src" +done +CLI_TOOL_ROOT="$(cd -P "$(dirname "$_src")/.." && pwd)" +export CLI_TOOL_ROOT +unset _src _dir diff --git a/bin/convert-video b/bin/convert-video new file mode 100644 index 0000000..470fb74 --- /dev/null +++ b/bin/convert-video @@ -0,0 +1,7 @@ +#!/bin/bash +# shellcheck source=bin/_bootstrap +source "${BASH_SOURCE[0]%/*}/_bootstrap" +# shellcheck source=lib/ffmpegcmds.sh +source "$CLI_TOOL_ROOT/lib/ffmpegcmds.sh" + +convert_vid "$1" diff --git a/bin/gitprune b/bin/gitprune index 9ce136b..f2c39db 100755 --- a/bin/gitprune +++ b/bin/gitprune @@ -1,11 +1,7 @@ #!/bin/bash +# shellcheck source=bin/_bootstrap +source "${BASH_SOURCE[0]%/*}/_bootstrap" # shellcheck source=lib/gitcmds.sh -_src="${BASH_SOURCE[0]}" -while [ -L "$_src" ]; do - _dir="$(cd -P "$(dirname "$_src")" && pwd)" - _src="$(readlink "$_src")" - [[ "$_src" != /* ]] && _src="$_dir/$_src" -done -source "$(cd -P "$(dirname "$_src")" && pwd)/../lib/gitcmds.sh" +source "$CLI_TOOL_ROOT/lib/gitcmds.sh" gitprune "$1" diff --git a/bin/gitrefresh b/bin/gitrefresh index bfaa9f4..f8f652e 100755 --- a/bin/gitrefresh +++ b/bin/gitrefresh @@ -1,11 +1,7 @@ #!/bin/bash +# shellcheck source=bin/_bootstrap +source "${BASH_SOURCE[0]%/*}/_bootstrap" # shellcheck source=lib/gitcmds.sh -_src="${BASH_SOURCE[0]}" -while [ -L "$_src" ]; do - _dir="$(cd -P "$(dirname "$_src")" && pwd)" - _src="$(readlink "$_src")" - [[ "$_src" != /* ]] && _src="$_dir/$_src" -done -source "$(cd -P "$(dirname "$_src")" && pwd)/../lib/gitcmds.sh" +source "$CLI_TOOL_ROOT/lib/gitcmds.sh" gitrefresh "$1" diff --git a/bin/sast b/bin/sast index db28660..bf40a42 100755 --- a/bin/sast +++ b/bin/sast @@ -1,8 +1,10 @@ #!/bin/bash set -euo pipefail -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -PLUGINS_DIR="${1:-$REPO_ROOT/plugins}" +# shellcheck source=bin/_bootstrap +source "${BASH_SOURCE[0]%/*}/_bootstrap" + +PLUGINS_DIR="${1:-$(pwd)/plugins}" FINDINGS=0 EXIT_CODE=0 @@ -22,7 +24,7 @@ while IFS= read -r file; do tools_line=$(awk '/^---/{f=!f; next} f && /^allowed-tools:/' "$file" | head -1) [[ -z "$tools_line" ]] && continue - rel="${file#"$REPO_ROOT"/}" + rel="${file#"$(pwd)"/}" # Bare Bash (no constraint) — allows any shell command if echo "$tools_line" | grep -qE '\bBash\b[^(]|Bash\s*\]|Bash\s*,'; then diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..5fdd73f --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +GREEN='\033[32m' +GREY='\033[2m' +RESET='\033[0m' + +gecho() { printf "${GREEN}%s${RESET}\n" "$*"; } + +show_progress() { + while IFS= read -r line; do + printf "\r${GREY}%-120s${RESET}" "${line:0:120}" + done + printf "\r%-120s\r" "" +} diff --git a/lib/ffmpegcmds.sh b/lib/ffmpegcmds.sh new file mode 100644 index 0000000..12278a4 --- /dev/null +++ b/lib/ffmpegcmds.sh @@ -0,0 +1,36 @@ +convert_vid() { + local silent=0 + local input="" + + for arg in "$@"; do + if [[ "$arg" == "--silent" ]]; then + silent=1 + else + input="$arg" + break + fi + done + + if ! command -v ffmpeg &>/dev/null; then + echo "convert_vid: ffmpeg is not installed. Please install ffmpeg to use this function." >&2 + return 1 + fi + + if [[ -z "$input" ]]; then + echo "convert_vid: usage: convert_vid [--silent] " >&2 + return 1 + fi + + if [[ ! -f "$input" ]]; then + echo "convert_vid: input file not found: $input" >&2 + return 1 + fi + + local output="${input%.*}.mp4" + + if [[ $silent -eq 1 ]]; then + ffmpeg -i "$input" -vcodec libx264 -crf 23 -preset medium -acodec aac -b:a 128k "$output" &>/dev/null + else + ffmpeg -i "$input" -vcodec libx264 -crf 23 -preset medium -acodec aac -b:a 128k "$output" + fi +} diff --git a/lib/gitcmds.sh b/lib/gitcmds.sh index 80e7f4e..7a8f540 100644 --- a/lib/gitcmds.sh +++ b/lib/gitcmds.sh @@ -1,17 +1,6 @@ #!/usr/bin/env bash - -GREEN='\033[32m' -GREY='\033[2m' -RESET='\033[0m' - -gecho() { printf "${GREEN}%s${RESET}\n" "$*"; } - -show_progress() { - while IFS= read -r line; do - printf "\r${GREY}%-120s${RESET}" "${line:0:120}" - done - printf "\r%-120s\r" "" -} +# shellcheck source=lib/common.sh +source "$CLI_TOOL_ROOT/lib/common.sh" gitprune() { gecho "Pruning local git branches" diff --git a/tests/test_bootstrap.bats b/tests/test_bootstrap.bats new file mode 100644 index 0000000..59134fd --- /dev/null +++ b/tests/test_bootstrap.bats @@ -0,0 +1,25 @@ +#!/usr/bin/env bats + +BOOTSTRAP="$BATS_TEST_DIRNAME/../bin/_bootstrap" + +@test "sources without error" { + run bash -c "source '$BOOTSTRAP'" + [ "$status" -eq 0 ] +} + +@test "sets CLI_TOOL_ROOT to a non-empty path" { + run bash -c "source '$BOOTSTRAP'; echo \"\$CLI_TOOL_ROOT\"" + [ "$status" -eq 0 ] + [[ "$output" != "" ]] +} + +@test "CLI_TOOL_ROOT contains bin/ and lib/" { + run bash -c "source '$BOOTSTRAP'; [ -d \"\$CLI_TOOL_ROOT/bin\" ] && [ -d \"\$CLI_TOOL_ROOT/lib\" ]" + [ "$status" -eq 0 ] +} + +@test "CLI_TOOL_ROOT is exported to child processes" { + run bash -c "source '$BOOTSTRAP'; bash -c 'echo \"\$CLI_TOOL_ROOT\"'" + [ "$status" -eq 0 ] + [[ "$output" != "" ]] +} diff --git a/tests/test_common.bats b/tests/test_common.bats new file mode 100644 index 0000000..687d547 --- /dev/null +++ b/tests/test_common.bats @@ -0,0 +1,34 @@ +#!/usr/bin/env bats + +LIB="$BATS_TEST_DIRNAME/../lib/common.sh" + +setup() { + # shellcheck source=../lib/common.sh + source "$LIB" +} + +@test "sources without error" { + true +} + +@test "gecho: outputs the provided text" { + run gecho "hello world" + [ "$status" -eq 0 ] + [[ "$output" == *"hello world"* ]] +} + +@test "gecho: accepts multiple words" { + run gecho "foo" "bar" + [ "$status" -eq 0 ] + [[ "$output" == *"foo"* ]] +} + +@test "show_progress: exits 0 on empty input" { + run bash -c "source '$LIB'; echo -n | show_progress" + [ "$status" -eq 0 ] +} + +@test "show_progress: consumes piped input without error" { + run bash -c "source '$LIB'; printf 'line1\nline2\n' | show_progress" + [ "$status" -eq 0 ] +} diff --git a/tests/test_gitcmds.bats b/tests/test_gitcmds.bats index ad4147f..725db01 100644 --- a/tests/test_gitcmds.bats +++ b/tests/test_gitcmds.bats @@ -22,6 +22,7 @@ EOF export PATH="$MOCK_BIN:$PATH" export GIT_LOG + export CLI_TOOL_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" # shellcheck source=../lib/gitcmds.sh source "$LIB" diff --git a/tests/test_sast.bats b/tests/test_sast.bats index 8f51cf4..60ddfbf 100644 --- a/tests/test_sast.bats +++ b/tests/test_sast.bats @@ -4,7 +4,8 @@ setup() { TMPDIR="$(mktemp -d)" mkdir -p "$TMPDIR/bin" "$TMPDIR/plugins/pkg" cp "$BATS_TEST_DIRNAME/../bin/sast" "$TMPDIR/bin/sast" - chmod +x "$TMPDIR/bin/sast" + cp "$BATS_TEST_DIRNAME/../bin/_bootstrap" "$TMPDIR/bin/_bootstrap" + chmod +x "$TMPDIR/bin/sast" "$TMPDIR/bin/_bootstrap" SAST="$TMPDIR/bin/sast" } @@ -12,6 +13,8 @@ teardown() { rm -rf "$TMPDIR" } +sast() { bash -c "cd '$TMPDIR' && '$SAST' \"\$@\"" -- "$@"; } + write_md() { cat > "$TMPDIR/plugins/pkg/agent.md" } @@ -23,7 +26,7 @@ allowed-tools: [Bash(echo hello), WebFetch(https://example.com)] --- body EOF - run "$SAST" + run sast [ "$status" -eq 0 ] [[ "$output" == *"Findings: 0"* ]] } @@ -34,7 +37,7 @@ EOF allowed-tools: [Bash, Read] --- EOF - run "$SAST" + run sast [ "$status" -eq 1 ] [[ "$output" == *"[ERROR]"* ]] [[ "$output" == *"bare 'Bash'"* ]] @@ -46,7 +49,7 @@ EOF allowed-tools: [Bash(*), Read] --- EOF - run "$SAST" + run sast [ "$status" -eq 1 ] [[ "$output" == *"Bash(*) is effectively unrestricted"* ]] } @@ -57,7 +60,7 @@ EOF allowed-tools: [WebFetch, Read] --- EOF - run "$SAST" + run sast [ "$status" -eq 0 ] [[ "$output" == *"[WARN]"* ]] [[ "$output" == *"bare 'WebFetch'"* ]] @@ -69,7 +72,7 @@ EOF allowed-tools: [*] --- EOF - run "$SAST" + run sast [ "$status" -eq 1 ] [[ "$output" == *"wildcard '*'"* ]] } @@ -80,7 +83,7 @@ EOF allowed-tools: [Bash(git status), Read] --- EOF - run "$SAST" + run sast [ "$status" -eq 0 ] [[ "$output" == *"Findings: 0"* ]] } @@ -91,7 +94,7 @@ EOF allowed-tools: [WebFetch(https://api.example.com), Read] --- EOF - run "$SAST" + run sast [ "$status" -eq 0 ] [[ "$output" == *"Findings: 0"* ]] } @@ -103,14 +106,14 @@ name: safe --- body text with allowed-tools: [Bash, *] EOF - run "$SAST" + run sast [ "$status" -eq 0 ] [[ "$output" == *"Findings: 0"* ]] } @test "no plugins dir: exits 0 with no findings" { rm -rf "$TMPDIR/plugins" - run "$SAST" + run sast [ "$status" -eq 0 ] [[ "$output" == *"Findings: 0"* ]] } @@ -121,7 +124,7 @@ EOF allowed-tools: [Bash, WebFetch] --- EOF - run "$SAST" + run sast [ "$status" -eq 1 ] [[ "$output" == *"Findings: 2"* ]] } From ee2032317fc1e3d160dc03c53e4f940553e9c089 Mon Sep 17 00:00:00 2001 From: Brandon Shoop Date: Tue, 2 Jun 2026 11:18:39 -0400 Subject: [PATCH 2/2] fix: pass all args to convert_vid so --silent works Co-Authored-By: Claude Sonnet 4.6 --- bin/convert-video | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/convert-video b/bin/convert-video index 470fb74..12825b6 100644 --- a/bin/convert-video +++ b/bin/convert-video @@ -4,4 +4,4 @@ source "${BASH_SOURCE[0]%/*}/_bootstrap" # shellcheck source=lib/ffmpegcmds.sh source "$CLI_TOOL_ROOT/lib/ffmpegcmds.sh" -convert_vid "$1" +convert_vid "$@"