Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ build/
!.env.example


change-summary.md
change-summary.md
wiki/**
!.gitkeep
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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 |
|----------|-------|
Expand All @@ -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.
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions bin/_bootstrap
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions bin/convert-video
Original file line number Diff line number Diff line change
@@ -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 "$@"
10 changes: 3 additions & 7 deletions bin/gitprune
Original file line number Diff line number Diff line change
@@ -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"
10 changes: 3 additions & 7 deletions bin/gitrefresh
Original file line number Diff line number Diff line change
@@ -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"
8 changes: 5 additions & 3 deletions bin/sast
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions lib/common.sh
Original file line number Diff line number Diff line change
@@ -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" ""
}
36 changes: 36 additions & 0 deletions lib/ffmpegcmds.sh
Original file line number Diff line number Diff line change
@@ -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] <input-file>" >&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
}
15 changes: 2 additions & 13 deletions lib/gitcmds.sh
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
25 changes: 25 additions & 0 deletions tests/test_bootstrap.bats
Original file line number Diff line number Diff line change
@@ -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" != "" ]]
}
34 changes: 34 additions & 0 deletions tests/test_common.bats
Original file line number Diff line number Diff line change
@@ -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 ]
}
1 change: 1 addition & 0 deletions tests/test_gitcmds.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 14 additions & 11 deletions tests/test_sast.bats
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ 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"
}

teardown() {
rm -rf "$TMPDIR"
}

sast() { bash -c "cd '$TMPDIR' && '$SAST' \"\$@\"" -- "$@"; }

write_md() {
cat > "$TMPDIR/plugins/pkg/agent.md"
}
Expand All @@ -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"* ]]
}
Expand All @@ -34,7 +37,7 @@ EOF
allowed-tools: [Bash, Read]
---
EOF
run "$SAST"
run sast
[ "$status" -eq 1 ]
[[ "$output" == *"[ERROR]"* ]]
[[ "$output" == *"bare 'Bash'"* ]]
Expand All @@ -46,7 +49,7 @@ EOF
allowed-tools: [Bash(*), Read]
---
EOF
run "$SAST"
run sast
[ "$status" -eq 1 ]
[[ "$output" == *"Bash(*) is effectively unrestricted"* ]]
}
Expand All @@ -57,7 +60,7 @@ EOF
allowed-tools: [WebFetch, Read]
---
EOF
run "$SAST"
run sast
[ "$status" -eq 0 ]
[[ "$output" == *"[WARN]"* ]]
[[ "$output" == *"bare 'WebFetch'"* ]]
Expand All @@ -69,7 +72,7 @@ EOF
allowed-tools: [*]
---
EOF
run "$SAST"
run sast
[ "$status" -eq 1 ]
[[ "$output" == *"wildcard '*'"* ]]
}
Expand All @@ -80,7 +83,7 @@ EOF
allowed-tools: [Bash(git status), Read]
---
EOF
run "$SAST"
run sast
[ "$status" -eq 0 ]
[[ "$output" == *"Findings: 0"* ]]
}
Expand All @@ -91,7 +94,7 @@ EOF
allowed-tools: [WebFetch(https://api.example.com), Read]
---
EOF
run "$SAST"
run sast
[ "$status" -eq 0 ]
[[ "$output" == *"Findings: 0"* ]]
}
Expand All @@ -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"* ]]
}
Expand All @@ -121,7 +124,7 @@ EOF
allowed-tools: [Bash, WebFetch]
---
EOF
run "$SAST"
run sast
[ "$status" -eq 1 ]
[[ "$output" == *"Findings: 2"* ]]
}