From 4c285681a65ae901bf56006c557924f04b151358 Mon Sep 17 00:00:00 2001 From: Nat Welch Date: Thu, 22 Oct 2026 22:53:12 +0000 Subject: [PATCH 1/6] fix --- bin/suitup | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/suitup b/bin/suitup index 78fdb6fc..fd6cccb0 100755 --- a/bin/suitup +++ b/bin/suitup @@ -72,9 +72,9 @@ if [ -d $PROJ ]; then done; fi -PROJ=~/HFP/ +PROJ=~/logo.dev/ if [ -d $PROJ ]; then - echo "# ~/HFP ------------------------------------------------------------------------------------" + echo "# ~/logo.dev ------------------------------------------------------------------------------------" for e in $(ls $PROJ | sort -f); do sync_dir $e if [ $? -eq 1 ]; then From f41b6f7e488d676a19f0de65fc4e14f65c47db81 Mon Sep 17 00:00:00 2001 From: Nat Welch Date: Thu, 22 Oct 2026 22:54:27 +0000 Subject: [PATCH 2/6] fix(bin): resolve shellcheck warnings in bin scripts Quote expansions in gifme, kubectl-ssh, and suitup, loop over project roots in suitup, and disable SC1071 so zsh scripts stop erroring in the LSP. Co-Authored-By: Claude Opus 5.5 --- .shellcheckrc | 1 + bin/gifme | 6 ++--- bin/kubectl-ssh | 12 ++++----- bin/suitup | 70 ++++++++++++++----------------------------------- 4 files changed, 29 insertions(+), 60 deletions(-) create mode 100644 .shellcheckrc diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 00000000..f4bd9e6e --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1 @@ +disable=SC1071 diff --git a/bin/gifme b/bin/gifme index 22d7bdfd..7fc00ed7 100755 --- a/bin/gifme +++ b/bin/gifme @@ -15,8 +15,8 @@ output="$HOME/Desktop/animated.gif" # Set delay to be 20 unless we set $DELAY delay=${DELAY-20} -convert -verbose -delay $delay -loop 0 -resize 500 -layers OptimizeTransparency \ - $* $output +convert -verbose -delay "$delay" -loop 0 -resize 500 -layers OptimizeTransparency \ + "$@" "$output" # comment out if you don't need CloudApp. -cloudapp $output +cloudapp "$output" diff --git a/bin/kubectl-ssh b/bin/kubectl-ssh index 549ba49c..89cebf39 100755 --- a/bin/kubectl-ssh +++ b/bin/kubectl-ssh @@ -6,7 +6,7 @@ ssh_node() { node=$1 shift if [ "$node" = "" ]; then - node=$(kubectl get node -o name $@ | sed 's/node\///' | tr '\n' ' ') + node=$(kubectl get node -o name "$@" | sed 's/node\///' | tr '\n' ' ') node=${node::-1} if [[ "$node" =~ " " ]]; then @@ -19,7 +19,7 @@ ssh_node() { ns=default pod=$( - kubectl create -n $ns -o name $@ -f - </dev/null - kubectl attach -n $ns -it $pod -c ssh-node $@ + kubectl wait -n "$ns" --for=condition=Ready --timeout 60s "$pod" "$@" >/dev/null + kubectl attach -n "$ns" -it "$pod" -c ssh-node "$@" } @@ -112,7 +112,7 @@ fi if [[ "$1" == node/* ]]; then nodeName=${1:5} shift - ssh_node $nodeName "$@" + ssh_node "$nodeName" "$@" elif [ "$1" == "node" ]; then shift case "$1" in diff --git a/bin/suitup b/bin/suitup index fd6cccb0..91ab090f 100755 --- a/bin/suitup +++ b/bin/suitup @@ -8,29 +8,29 @@ sync_dir () { f=$1 - if [ -d $PROJ$f ]; then + if [ -d "$PROJ$f" ]; then echo "--------> $f" - cd $PROJ$f; + cd "$PROJ$f" || return 2 rm -f .DS_Store - if [ -d $PROJ$f/.git/ ]; then + if [ -d "$PROJ$f/.git/" ]; then git fetch --prune branch=$(git symbolic-ref --short HEAD 2>/dev/null) track=$(git for-each-ref --format='%(upstream:track)' "refs/heads/$branch" 2>/dev/null) - upstream=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null) + upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) if [ "$track" = "[gone]" ] || [ -z "$upstream" ]; then remote=$(git config "branch.$branch.remote") [ -n "$remote" ] || remote=origin default=$(git symbolic-ref --short "refs/remotes/$remote/HEAD" 2>/dev/null) \ || { git remote set-head "$remote" -a >/dev/null 2>&1 && default=$(git symbolic-ref --short "refs/remotes/$remote/HEAD" 2>/dev/null); } - if [ -n "$default" ] && [ "${default#$remote/}" != "$branch" ]; then - echo "Upstream for $branch is gone/unresolvable; switching to ${default#$remote/}." - git checkout "${default#$remote/}" + if [ -n "$default" ] && [ "${default#"$remote"/}" != "$branch" ]; then + echo "Upstream for $branch is gone/unresolvable; switching to ${default#"$remote"/}." + git checkout "${default#"$remote"/}" fi fi git pull git status -sb return 0; - elif [ -d $PROJ$f/.svn/ ]; then + elif [ -d "$PROJ$f/.svn/" ]; then svn up return 0; else @@ -39,48 +39,16 @@ sync_dir () { fi; } -PROJ=~/Projects/ -if [ -d $PROJ ]; then - echo "# ~/Projects --------------------------------------------------------------------------------" - for e in $(ls $PROJ | sort -f); do - sync_dir $e - if [ $? -eq 1 ]; then - echo "No known version control." - fi - done; -fi - -PROJ=~/Sites/ -if [ -d $PROJ ]; then - echo "# ~/Sites -----------------------------------------------------------------------------------" - for e in $(ls $PROJ | sort -f); do - sync_dir $e - if [ $? -eq 1 ]; then - echo "No known version control." - fi - done; -fi - -PROJ=~/Work/ -if [ -d $PROJ ]; then - echo "# ~/Work ------------------------------------------------------------------------------------" - for e in $(ls $PROJ | sort -f); do - sync_dir $e - if [ $? -eq 1 ]; then - echo "No known version control." - fi - done; -fi - -PROJ=~/logo.dev/ -if [ -d $PROJ ]; then - echo "# ~/logo.dev ------------------------------------------------------------------------------------" - for e in $(ls $PROJ | sort -f); do - sync_dir $e - if [ $? -eq 1 ]; then - echo "No known version control." - fi - done; -fi +for PROJ in ~/Projects/ ~/Sites/ ~/Work/ ~/logo.dev/; do + if [ -d "$PROJ" ]; then + printf '# %s %s\n' "${PROJ%/}" "--------------------------------------------------------------------------------" + while IFS= read -r e; do + sync_dir "$e" + if [ $? -eq 1 ]; then + echo "No known version control." + fi + done < <(find "$PROJ" -mindepth 1 -maxdepth 1 -exec basename {} \; | sort -f) + fi +done echo "All Done!" From 5b9e5f2055ff3e1ead8a775a184d49e4f3696b6b Mon Sep 17 00:00:00 2001 From: Nat Welch Date: Thu, 22 Oct 2026 22:56:19 +0000 Subject: [PATCH 3/6] fix(bin): disable SC1071 inline in zsh scripts .shellcheckrc is not found when an LSP lints via stdin from another directory. Co-Authored-By: Claude Opus 5.5 --- .shellcheckrc | 1 - bin/brewup | 1 + bin/clean.sh | 1 + bin/daily | 1 + bin/dbr | 1 + bin/git-adopt | 1 + bin/infra-script | 1 + bin/screenshots | 1 + bin/set_ip | 1 + 9 files changed, 8 insertions(+), 1 deletion(-) delete mode 100644 .shellcheckrc diff --git a/.shellcheckrc b/.shellcheckrc deleted file mode 100644 index f4bd9e6e..00000000 --- a/.shellcheckrc +++ /dev/null @@ -1 +0,0 @@ -disable=SC1071 diff --git a/bin/brewup b/bin/brewup index 16a04f53..9e4fed03 100755 --- a/bin/brewup +++ b/bin/brewup @@ -1,4 +1,5 @@ #! /bin/zsh +# shellcheck disable=SC1071 brew update brew upgrade -y diff --git a/bin/clean.sh b/bin/clean.sh index 3be1c835..75a43d23 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -1,4 +1,5 @@ #! /bin/zsh +# shellcheck disable=SC1071 set -x diff --git a/bin/daily b/bin/daily index e489d3c6..38478030 100755 --- a/bin/daily +++ b/bin/daily @@ -1,4 +1,5 @@ #! /bin/zsh +# shellcheck disable=SC1071 cd ~/Projects/random diff --git a/bin/dbr b/bin/dbr index cc231ff4..36c575fa 100755 --- a/bin/dbr +++ b/bin/dbr @@ -1,4 +1,5 @@ #! /bin/zsh +# shellcheck disable=SC1071 docker run --rm -t $@ $(docker build -q .) diff --git a/bin/git-adopt b/bin/git-adopt index 793c03c3..9a09b3a7 100755 --- a/bin/git-adopt +++ b/bin/git-adopt @@ -1,4 +1,5 @@ #! /bin/zsh +# shellcheck disable=SC1071 function reset() { git config --local --unset-all user.email diff --git a/bin/infra-script b/bin/infra-script index 2a905753..ff0f6064 100755 --- a/bin/infra-script +++ b/bin/infra-script @@ -1,4 +1,5 @@ #! /bin/zsh +# shellcheck disable=SC1071 # shellcheck shell=bash # This script changes directory to the related infra directory if it exists. diff --git a/bin/screenshots b/bin/screenshots index 38f03146..7bad53bc 100755 --- a/bin/screenshots +++ b/bin/screenshots @@ -1,4 +1,5 @@ #! /bin/zsh +# shellcheck disable=SC1071 mv -v ~/Desktop/Screen\ Recording* ~/Dropbox/Screenshots/ mv -v ~/Desktop/Screen\ Shot* ~/Dropbox/Screenshots/ diff --git a/bin/set_ip b/bin/set_ip index 592799b5..5c455ac6 100755 --- a/bin/set_ip +++ b/bin/set_ip @@ -1,4 +1,5 @@ #! /bin/zsh +# shellcheck disable=SC1071 IP=$(curl -s 'https://api.ipify.org') maindomain=$(expr match "$1" '.*\.\(.*\..*\)') From 1b54241d8324488529f50440a471a040e755f2fb Mon Sep 17 00:00:00 2001 From: Nat Welch Date: Thu, 22 Oct 2026 23:00:18 +0000 Subject: [PATCH 4/6] fix(bin): run portable zsh scripts under bash and lint them Switch brewup, daily, dbr, git-adopt, set_ip, and screenshots to bash so shellcheck covers them, fix the findings, and syntax check the remaining zsh scripts with zsh -n in CI. CI now only lints shell files and fails on findings. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 9 +++++++-- bin/brewup | 18 +++++++++--------- bin/daily | 5 ++--- bin/dbr | 5 ++--- bin/git-adopt | 3 +-- bin/infra-script | 1 - bin/screenshots | 3 +-- bin/set_ip | 11 +++++------ 8 files changed, 27 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eed4d9ce..e3465cef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,7 @@ jobs: uses: actions/checkout@v7 - name: Run shellcheck on bin/ directory run: | - # Check all files in bin/ directory (all are shell scripts) - find bin/ -type f -exec shellcheck {} \; + grep -lE '^#!.*(ba|z)?sh$' bin/* | xargs shellcheck - name: Run shellcheck on oh-my-zsh custom directory run: | # Check .zsh files in custom directory @@ -26,6 +25,12 @@ jobs: if [ -d "link/oh-my-zsh/custom/hosts" ]; then find link/oh-my-zsh/custom/hosts -name "*.zsh" -type f -exec shellcheck -s bash {} \; fi + - name: Install zsh + run: | + sudo apt-get update && sudo apt-get install -y zsh + - name: Syntax check zsh scripts in bin/ + run: | + grep -lE '^#!.*zsh$' bin/* | xargs -n1 zsh -n go-test: name: Go Tests runs-on: ubuntu-latest diff --git a/bin/brewup b/bin/brewup index 9e4fed03..f6df1e66 100755 --- a/bin/brewup +++ b/bin/brewup @@ -1,16 +1,16 @@ -#! /bin/zsh -# shellcheck disable=SC1071 +#!/usr/bin/env bash brew update brew upgrade -y brew cleanup -cd ~/Projects/dotfiles && \ - git pull && - brew bundle && \ - git add Brewfile.lock.json && \ - git ci -m 'update brewfile' && \ - git push && \ - cd - +( + cd ~/Projects/dotfiles && + git pull && + brew bundle && + git add Brewfile.lock.json && + git ci -m 'update brewfile' && + git push +) gcloud components update -q diff --git a/bin/daily b/bin/daily index 38478030..f226a8f5 100755 --- a/bin/daily +++ b/bin/daily @@ -1,6 +1,5 @@ -#! /bin/zsh -# shellcheck disable=SC1071 +#!/usr/bin/env bash -cd ~/Projects/random +cd ~/Projects/random || exit 1 claude ~/Projects/random/Prompts/daily.md diff --git a/bin/dbr b/bin/dbr index 36c575fa..97d55e98 100755 --- a/bin/dbr +++ b/bin/dbr @@ -1,5 +1,4 @@ -#! /bin/zsh -# shellcheck disable=SC1071 +#!/usr/bin/env bash -docker run --rm -t $@ $(docker build -q .) +docker run --rm -t "$@" "$(docker build -q .)" diff --git a/bin/git-adopt b/bin/git-adopt index 9a09b3a7..811e859e 100755 --- a/bin/git-adopt +++ b/bin/git-adopt @@ -1,5 +1,4 @@ -#! /bin/zsh -# shellcheck disable=SC1071 +#!/usr/bin/env bash function reset() { git config --local --unset-all user.email diff --git a/bin/infra-script b/bin/infra-script index ff0f6064..2a905753 100755 --- a/bin/infra-script +++ b/bin/infra-script @@ -1,5 +1,4 @@ #! /bin/zsh -# shellcheck disable=SC1071 # shellcheck shell=bash # This script changes directory to the related infra directory if it exists. diff --git a/bin/screenshots b/bin/screenshots index 7bad53bc..2930e124 100755 --- a/bin/screenshots +++ b/bin/screenshots @@ -1,5 +1,4 @@ -#! /bin/zsh -# shellcheck disable=SC1071 +#!/usr/bin/env bash mv -v ~/Desktop/Screen\ Recording* ~/Dropbox/Screenshots/ mv -v ~/Desktop/Screen\ Shot* ~/Dropbox/Screenshots/ diff --git a/bin/set_ip b/bin/set_ip index 5c455ac6..90e61218 100755 --- a/bin/set_ip +++ b/bin/set_ip @@ -1,8 +1,7 @@ -#! /bin/zsh -# shellcheck disable=SC1071 +#!/usr/bin/env bash IP=$(curl -s 'https://api.ipify.org') -maindomain=$(expr match "$1" '.*\.\(.*\..*\)') +maindomain=$(expr "$1" : '.*\.\(.*\..*\)') subdomain=${1%".$maindomain"} if [[ -z "$IP" ]]; then @@ -10,9 +9,9 @@ if [[ -z "$IP" ]]; then exit 1 fi -cd ~/Projects/icco.me/$maindomain/ +mkdir -p ~/Projects/icco.me/"$maindomain"/ +cd ~/Projects/icco.me/"$maindomain"/ || exit 1 git pull -mkdir -p ~/Projects/icco.me/$maindomain/ -echo $IP > ~/Projects/icco.me/$maindomain/$subdomain +echo "$IP" > ~/Projects/icco.me/"$maindomain"/"$subdomain" git add . git diff --staged --quiet || git commit -m 'chore: set ip' && git push -u From b637f570bb7623adf60d3f1c87f7e1ee33cb7022 Mon Sep 17 00:00:00 2001 From: Nat Welch Date: Thu, 22 Oct 2026 23:04:33 +0000 Subject: [PATCH 5/6] feat(bin): add photo uploader and use it in gifme photo posts images to photos.natwelch.com, prints the URLs, and copies them to the clipboard. gifme now builds with magick and uploads through photo instead of CloudApp. Co-Authored-By: Claude Opus 5.5 --- bin/gifme | 10 +++++----- bin/photo | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) create mode 100755 bin/photo diff --git a/bin/gifme b/bin/gifme index 7fc00ed7..db43e450 100755 --- a/bin/gifme +++ b/bin/gifme @@ -3,20 +3,20 @@ # GIF ME # as in: "GIF ME AN ANIMATED GIF, DAMMIT" # -# Use imagemagick to make an animated gif of awesome. Upload to CloudApp. +# Use imagemagick to make an animated gif of awesome. Upload to photos.natwelch.com. # # Examples: # # gifme ~/Desktop/rtomayko.png ~/Desktop/pimpcane.jpg # gifme ~/IncriminatingPhotosOfKyle/*.jpg +set -e + output="$HOME/Desktop/animated.gif" # Set delay to be 20 unless we set $DELAY delay=${DELAY-20} -convert -verbose -delay "$delay" -loop 0 -resize 500 -layers OptimizeTransparency \ - "$@" "$output" +magick -delay "$delay" -loop 0 "$@" -resize 500 -layers OptimizeTransparency "$output" -# comment out if you don't need CloudApp. -cloudapp "$output" +photo "$output" diff --git a/bin/photo b/bin/photo new file mode 100755 index 00000000..cad96734 --- /dev/null +++ b/bin/photo @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# +# Upload images to photos.natwelch.com and print their URLs. +# +# Examples: +# +# photo ~/Desktop/cat.jpg +# photo ~/Desktop/*.png + +set -euo pipefail + +if [ $# -eq 0 ]; then + echo "usage: photo FILE..." >&2 + exit 1 +fi + +args=() +for f in "$@"; do + args+=(-F "photo=@$f") +done + +urls=$(curl -fsS "${args[@]}" https://photos.natwelch.com/api/upload | jq -r '.files[].url') + +if [ -z "$urls" ]; then + echo "photo: nothing uploaded, only image files are accepted" >&2 + exit 1 +fi + +printf '%s\n' "$urls" +if command -v pbcopy >/dev/null; then + printf '%s' "$urls" | pbcopy +fi From b5c65c1a21926a5faf509e893991c0d7f8df3983 Mon Sep 17 00:00:00 2001 From: Nat Welch Date: Thu, 22 Oct 2026 23:08:28 +0000 Subject: [PATCH 6/6] feat(bin): add video uploader and quote upload filenames video posts to videos.natwelch.com and prints the watch URLs. Both uploaders quote filenames so commas and semicolons reach the server intact, and print the server's error message on failure. Co-Authored-By: Claude Opus 5.5 --- bin/photo | 9 +++++++-- bin/video | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100755 bin/video diff --git a/bin/photo b/bin/photo index cad96734..466f853b 100755 --- a/bin/photo +++ b/bin/photo @@ -16,10 +16,15 @@ fi args=() for f in "$@"; do - args+=(-F "photo=@$f") + f=${f//\\/\\\\} + args+=(-F "photo=@\"${f//\"/\\\"}\"") done -urls=$(curl -fsS "${args[@]}" https://photos.natwelch.com/api/upload | jq -r '.files[].url') +body=$(curl -sS --fail-with-body "${args[@]}" https://photos.natwelch.com/api/upload) || { + jq -r '.message // empty' <<<"$body" >&2 + exit 1 +} +urls=$(jq -r '.files[].url' <<<"$body") if [ -z "$urls" ]; then echo "photo: nothing uploaded, only image files are accepted" >&2 diff --git a/bin/video b/bin/video new file mode 100755 index 00000000..2250ddba --- /dev/null +++ b/bin/video @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Upload videos to videos.natwelch.com and print their watch URLs. Each URL +# works once the background WebM conversion finishes. +# +# Examples: +# +# video ~/Desktop/demo.mov +# video ~/Movies/*.mp4 + +set -euo pipefail + +if [ $# -eq 0 ]; then + echo "usage: video FILE..." >&2 + exit 1 +fi + +args=() +for f in "$@"; do + f=${f//\\/\\\\} + args+=(-F "video=@\"${f//\"/\\\"}\"") +done + +body=$(curl -sS --fail-with-body "${args[@]}" https://videos.natwelch.com/api/upload) || { + jq -r '.message // empty' <<<"$body" >&2 + exit 1 +} +year=$(date -u +%Y) +urls=$(jq -r --arg year "$year" '.jobs[] | "https://videos.natwelch.com/videos/\($year)/\(.id | sub("\\.[^.]+$"; "")).webm"' <<<"$body") + +printf '%s\n' "$urls" +if command -v pbcopy >/dev/null; then + printf '%s' "$urls" | pbcopy +fi