shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents

- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
This commit is contained in:
Prad Nukala committed 2026-08-10 15:15:06 -04:00
1 parent 6776366ce6
commit 6406208b35
637 files changed
+100297 -497

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
# Choose and launch an AI CLI via gum.
choice=$(gum choose --header 'AI CLI' claude opencode codex hermes fractal) || exit 0
[ -n "$choice" ] || exit 0
case "$choice" in
claude) exec claude --dangerously-skip-permissions ;;
# labels match the binary names, so just run it;
# add a dedicated case here if another tool needs flags
*) exec "$choice" ;;
esac
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
# Code2Prompt with fuzzy-search template.
tpl=$(fd --type f --extension hbs . "$HOME/.config/code2prompt" | fzf) || exit 0
[ -n "$tpl" ] || exit 0
exec code2prompt --template "$tpl"
+43
View File
@@ -0,0 +1,43 @@
#!/bin/sh
# Fuzzy-pick a desktop app ID and copy to clipboard.
existing_dirs=""
for d in /usr/share/applications \
/usr/local/share/applications \
"$HOME/.local/share/applications" \
/var/lib/flatpak/exports/share/applications \
"$HOME/.local/share/flatpak/exports/share/applications" \
/var/lib/snapd/desktop/applications; do
[ -d "$d" ] && existing_dirs="$existing_dirs $d"
done
if [ -z "$existing_dirs" ]; then
echo "No application directories found." >&2
exit 1
fi
# word splitting of $existing_dirs is intended
# shellcheck disable=SC2086
chosen=$(
fd --type f --extension desktop . $existing_dirs \
| while IFS= read -r f; do
app_id=$(basename "$f" .desktop)
name=$(sed -n 's/^Name=//p' "$f" | head -n1)
[ -z "$name" ] && name=$app_id
printf '%s\t%s\n' "$name" "$app_id"
done \
| sort -u -t "$(printf '\t')" -k1 \
| fzf \
--delimiter='\t' \
--with-nth=1,2 \
--prompt="App ID › " \
--height=50% \
--layout=reverse \
--border=rounded \
--info=inline
)
[ -n "$chosen" ] || exit 0
app_id=$(printf '%s\n' "$chosen" | cut -f2)
printf '%s' "$app_id" | wl-copy
echo "Copied: $app_id"
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# Copy all AGENTS.md files to a target directory while preserving structure.
out_dir=$1
if [ -z "$out_dir" ]; then
echo "Error: Output directory required." >&2
echo "Usage: extract_agents <absolute_or_relative_output_path>" >&2
exit 1
fi
mkdir -p "$out_dir"
# Find and copy preserving relative paths
fd -t f -g "AGENTS.md" -x cp --parents "{}" "$out_dir"
echo "Extraction complete. Check $out_dir."
+44
View File
@@ -0,0 +1,44 @@
#!/bin/sh
# Fuzzy find and execute a Fabric pattern.
json_file=$HOME/.config/fabric/pattern_explainations.json
if [ ! -f "$json_file" ]; then
printf '\033[31mError: %s not found.\033[0m\n' "$json_file" >&2
exit 1
fi
# fabric is aliased to fabric-ai in the shells; resolve the real binary here
if command -v fabric-ai >/dev/null 2>&1; then
fabric_bin=fabric-ai
else
fabric_bin=fabric
fi
# 1. Safely capture piped input (if any) to a temp file — preserves all
# formatting and newlines, avoids variable escaping issues
temp_input=$(mktemp)
trap 'rm -f "$temp_input"' EXIT INT TERM
if [ ! -t 0 ]; then
cat > "$temp_input"
fi
# 2. Parse JSON to TSV -> fzf (name in list, description in preview)
selection=$(jq -r '.[] | "\(.name)\t\(.description)"' "$json_file" \
| fzf --delimiter '\t' \
--with-nth 1 \
--preview 'echo {2}' \
--preview-window 'right:60%:wrap' \
--prompt 'Pattern > ' \
--info inline \
--bind 'ctrl-/:toggle-preview')
# 3. Execute with or without the piped data
if [ -n "$selection" ]; then
pattern=$(printf '%s\n' "$selection" | cut -f1)
if [ -s "$temp_input" ]; then
"$fabric_bin" --pattern "$pattern" < "$temp_input"
else
"$fabric_bin" --pattern "$pattern"
fi
fi
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# Discard all git changes with gum confirmation.
if gum confirm "Discard all uncommitted changes? This is irreversible."; then
git reset --hard HEAD
git clean -fd
echo "Changes wiped."
else
echo "Operation aborted."
fi
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env bash
# gh-sonr-org-migrate
# Migrates all repos from orgs matching "sonr" into sonr-io,
# prompting via gum for renames on collision, then deletes emptied orgs.
#
# Requires: gh (authenticated as org owner on all orgs), gum, jq
TARGET_ORG=sonr-io
red() { printf '\033[31m%s\033[0m\n' "$*"; }
green() { printf '\033[32m%s\033[0m\n' "$*"; }
yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
cyan() { printf '\033[36m%s\033[0m\n' "$*"; }
bcyan() { printf '\033[1;36m%s\033[0m\n' "$*"; }
bgreen() { printf '\033[1;32m%s\033[0m\n' "$*"; }
# ── Preflight ────────────────────────────────────────────────────────────────
for bin in gh gum jq; do
if ! command -v "$bin" >/dev/null 2>&1; then
red "missing dependency: $bin"
exit 1
fi
done
if ! gh auth status >/dev/null 2>&1; then
red "gh is not authenticated"
exit 1
fi
# ── Discover source orgs ─────────────────────────────────────────────────────
# All orgs the user belongs to whose login contains "sonr", excluding target.
mapfile -t source_orgs < <(gh api user/orgs --paginate --jq '.[].login' \
| grep 'sonr' | grep -vx "$TARGET_ORG")
if [ "${#source_orgs[@]}" -eq 0 ]; then
echo "No source orgs found. Nothing to do."
exit 0
fi
cyan "Source orgs:"
printf ' %s\n' "${source_orgs[@]}"
cyan "Target org:"
echo " $TARGET_ORG"
echo
# Let the user confirm / trim the list interactively
mapfile -t selected_orgs < <(printf '%s\n' "${source_orgs[@]}" | gum choose --no-limit \
--header "Select orgs to migrate into $TARGET_ORG (space to toggle, enter to confirm)")
if [ "${#selected_orgs[@]}" -eq 0 ]; then
echo "Nothing selected. Exiting."
exit 0
fi
# ── Helpers ──────────────────────────────────────────────────────────────────
repo_exists_in_target() {
gh api "repos/$TARGET_ORG/$1" --silent >/dev/null 2>&1
}
transfer_repo() {
# $1 = source org, $2 = repo name, $3 = (optional) new name
local src=$1 repo=$2 new_name=${3:-}
local args=(-f "new_owner=$TARGET_ORG")
if [ -n "$new_name" ]; then
args+=(-f "new_name=$new_name")
fi
gh api -X POST "repos/$src/$repo/transfer" "${args[@]}" --silent
}
# ── Migration ────────────────────────────────────────────────────────────────
failed_transfers=()
for org in "${selected_orgs[@]}"; do
echo
bcyan "── $org ──"
mapfile -t repos < <(gh api "orgs/$org/repos" --paginate --jq '.[].name')
if [ "${#repos[@]}" -eq 0 ]; then
echo " (no repos)"
continue
fi
for repo in "${repos[@]}"; do
new_name=""
if repo_exists_in_target "$repo"; then
yellow " ⚠ $repo already exists in $TARGET_ORG"
while true; do
new_name=$(gum input \
--header "New name for $org/$repo (exists in $TARGET_ORG)" \
--placeholder "$org-$repo" \
--value "$org-$repo")
if [ -z "$new_name" ]; then
echo " skipped $org/$repo"
new_name=__SKIP__
break
fi
if repo_exists_in_target "$new_name"; then
yellow " $new_name also exists — pick another"
else
break
fi
done
[ "$new_name" = __SKIP__ ] && continue
fi
label="$org/$repo → $TARGET_ORG/${new_name:-$repo}"
if transfer_repo "$org" "$repo" "$new_name"; then
green " ✓ $label"
else
red " ✗ failed: $label"
failed_transfers+=("$org/$repo")
fi
done
done
if [ "${#failed_transfers[@]}" -gt 0 ]; then
echo
red "Failed transfers (orgs containing these will NOT be deleted):"
printf ' %s\n' "${failed_transfers[@]}"
fi
# ── Wait for transfers to settle ─────────────────────────────────────────────
# GitHub repo transfers are asynchronous; give them time to drain.
echo
gum spin --title "Waiting 30s for transfers to settle..." -- sleep 30
# ── Delete emptied orgs ──────────────────────────────────────────────────────
echo
for org in "${selected_orgs[@]}"; do
# Skip orgs with failed transfers
org_had_failure=0
for f in ${failed_transfers[@]+"${failed_transfers[@]}"}; do
case "$f" in
"$org"/*) org_had_failure=1; break ;;
esac
done
if [ "$org_had_failure" -eq 1 ]; then
yellow "skipping delete: $org (had failures)"
continue
fi
remaining=$(gh api "orgs/$org/repos" --paginate --jq '. | length' | jq -s 'add // 0')
if [ "$remaining" -gt 0 ]; then
yellow "skipping delete: $org still has $remaining repo(s) (transfer pending?)"
continue
fi
if gum confirm "Delete empty org $org?"; then
if gh api -X DELETE "orgs/$org" --silent; then
green "✓ deleted $org"
else
red "✗ could not delete $org (API delete requires org owner; may need manual deletion in Settings)"
fi
else
echo "kept $org"
fi
done
echo
bgreen "Done."
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# ghq get + resolve the on-disk clone path; prints the repo directory.
# Shell wrappers (fish/bash/zsh `ghg` functions) cd into the printed path —
# a child process can't change the caller's cwd.
repo=$1
if [ -z "$repo" ]; then
echo "usage: ghg-clone <repo>" >&2
exit 1
fi
# ghq natively understands https://, ssh://, git@host:…, and host/user/repo.
# Keep its output on stderr so stdout stays clean for the wrapper's cd.
ghq get "$repo" >&2 || exit
# Reduce the input to ghq's on-disk shape: <host>/<user>/<repo> —
# strip scheme (https://, ssh://, git://), user@ (git@github.com:…),
# turn scp host:path into host/path, drop trailing .git.
rel=$(printf '%s\n' "$repo" \
| sed -E 's#^[[:alnum:]]+://##; s#^[^@/]+@##; s#:#/#; s#\.git$##')
# bare "user/repo" with no host → default to github.com
case "$rel" in
*.*/*) ;;
*) rel=github.com/$rel ;;
esac
dir=$(ghq root)/$rel
if [ ! -d "$dir" ]; then
echo "ghg: expected clone at $dir but it does not exist" >&2
exit 1
fi
command -v zoxide >/dev/null 2>&1 && zoxide add "$dir"
printf '%s\n' "$dir"
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
# Create a new private repo (prad.codes via tea, or github.com via gh),
# clone it with ghq, and print the clone directory for the shell wrapper to cd.
repo=$1
if [ -z "$repo" ]; then
echo "usage: ghn <repo-name>" >&2
exit 1
fi
host=$(gum choose "prad.codes" "github.com") || exit 1
if [ "$host" = prad.codes ]; then
owner=$(tea organizations list -o simple | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | gum choose) || exit 1
[ -n "$owner" ] || exit 1
tea repos create --name "$repo" --owner "$owner" --private >&2 || exit 1
ghq get "https://prad.codes/$owner/$repo" >&2 || exit 1
printf '%s\n' "$(ghq root)/prad.codes/$owner/$repo"
else
owner=$(gum choose "prdlk" "wltbs") || exit 1
[ -n "$owner" ] || exit 1
gh repo create "$owner/$repo" --private >&2 || exit 1
ghq get "github.com/$owner/$repo" >&2 || exit 1
printf '%s\n' "$(ghq root)/github.com/$owner/$repo"
fi
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
# Pick a ghq repo or an Obsidian vault with fzf (tab toggles the list);
# prints the directory for the shell wrapper to cd into.
vaults="$HOME/Documents/Obsidian/Personal $HOME/Documents/Obsidian/Walletbase $HOME/Documents/Obsidian/Research"
# fzf runs bind actions with $SHELL -c; pin it to sh so the transform
# snippet below is interpreted consistently regardless of login shell.
selection=$(ghq list | SHELL=/bin/sh fzf --prompt="repo: " --tiebreak=end \
--bind "tab:transform:if [ \"\$FZF_PROMPT\" = 'repo: ' ]; then
echo \"change-prompt(vault: )+reload(printf '%s\\n' $vaults)\"
else
echo 'change-prompt(repo: )+reload(ghq list)'
fi") || exit 1
[ -n "$selection" ] || exit 1
# Vault entries are absolute paths; ghq entries are host/user/repo
case "$selection" in
/*) printf '%s\n' "$selection" ;;
*) printf '%s\n' "$(ghq root)/$selection" ;;
esac
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
# Aggregate all AGENTS.md files in the current git repo via code2prompt.
repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
if [ -z "$repo_root" ]; then
echo "git-summary: not inside a git repository" >&2
exit 1
fi
template=$HOME/.config/code2prompt/agents-md.hbs
if [ ! -f "$template" ]; then
echo "git-summary: template not found at $template" >&2
exit 1
fi
# Pass through any extra flags (e.g. --output-file, --tokens)
exec code2prompt "$repo_root" \
--include "AGENTS.md" \
--include "**/AGENTS.md" \
--template "$template" \
"$@"
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# Overlay omp on the current kitty window with a livediff split beside it.
#
# kitty appends -PID to config-file listen_on sockets, so a hardcoded
# --to unix:@kitty-local never matches. kitty @ resolves the real
# address from $KITTY_LISTEN_ON on its own.
if [ -z "${KITTY_LISTEN_ON:-}" ]; then
echo "launch_omp_livediff_overlay: no kitty remote-control socket (KITTY_LISTEN_ON unset)" >&2
exit 1
fi
# omp overlays the current window; capture its window id
wid=$(kitty @ launch --type=overlay-main --cwd=current omp) || exit
# livediff side by side in the same tab; keep focus on omp
exec kitty @ launch --match "id:$wid" --location=vsplit --keep-focus --cwd=current livediff
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
# fzf-pick LeetCode solution files and run /leetcode-eli5 on each via omp.
root=${1:-.}
# Match only numbered problem files: 1365.some-slug.js anywhere in the tree
files=$(fd --type f '^\d+\..+\.(js|ts)$' "$root" \
| fzf --multi \
--prompt='eli5> ' \
--delimiter=/ \
--with-nth=-3.. \
--preview 'bat --color=always --line-range=:40 {}' \
--preview-window=right:60%)
[ -n "$files" ] || exit 1
printf '%s\n' "$files" | while IFS= read -r f; do
echo "── /leetcode-eli5 $f"
omp -p "/leetcode-eli5 $f"
done
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# Fetch a URL as markdown, copy to clipboard, preview with mdfried.
url=$1
if [ -z "$url" ]; then
echo "usage: mdcp <url>" >&2
exit 1
fi
if command -v wl-copy >/dev/null 2>&1; then
copy=wl-copy paste=wl-paste
elif command -v pbcopy >/dev/null 2>&1; then
copy=pbcopy paste=pbpaste
else
echo "mdcp: no clipboard tool (wl-copy/pbcopy) found" >&2
exit 1
fi
curl.md "$url" | "$copy"
"$paste" | mdfried
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
# Interactive Markdown selector and previewer.
file=$(fd --type f --extension md --max-depth 3 \
| fzf --preview 'mdfried {}' --preview-window=right:60%:wrap) || exit 0
[ -n "$file" ] || exit 0
exec mdfried "$file"
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# fzf package.json script runner with auto-detected package manager.
# ── Locate nearest package.json (walk up) ──────────────────────────
dir=$PWD
pkg=""
while :; do
if [ -f "$dir/package.json" ]; then
pkg="$dir/package.json"
break
fi
[ "$dir" = / ] && break
dir=$(dirname "$dir")
done
if [ -z "$pkg" ]; then
echo "pj: no package.json found in this tree" >&2
exit 1
fi
root=$(dirname "$pkg")
# ── Detect package manager ─────────────────────────────────────────
# Priority: packageManager field (corepack) → lockfiles (walking up
# past $root to catch monorepo-root locks) → npm fallback
pm=$(jq -r '.packageManager // empty' "$pkg")
pm=${pm%%@*}
if [ -z "$pm" ]; then
d=$root
while [ -z "$pm" ]; do
if [ -f "$d/bun.lockb" ] || [ -f "$d/bun.lock" ]; then
pm=bun
elif [ -f "$d/pnpm-lock.yaml" ]; then
pm=pnpm
elif [ -f "$d/yarn.lock" ]; then
pm=yarn
elif [ -f "$d/package-lock.json" ]; then
pm=npm
fi
[ "$d" = / ] && break
d=$(dirname "$d")
done
[ -n "$pm" ] || pm=npm
fi
# ── Pick a script ──────────────────────────────────────────────────
script=$(jq -r '.scripts // {} | keys[]' "$pkg" | fzf \
--prompt="$pm run ❯ " \
--header="$root" \
--height=40% --layout=reverse --border \
--preview="jq -r --arg s {} '.scripts[\$s]' \"$pkg\"" \
--preview-window=up:2:wrap)
[ -n "$script" ] || exit 0
# ── Run (npm needs `--` to forward extra args) ────────────────────
extra=("$@")
if [ "$pm" = npm ] && [ $# -gt 0 ]; then
extra=(-- "$@")
fi
# Wrap long-running server scripts in openlogs (live log tail for agents).
# Matches dev/start/serve and namespaced variants like dev:web, start:api.
wrapper=""
if [[ $script =~ ^(dev|start|serve|watch)(:.*)?$ ]]; then
# Resolve the binary: PATH → mise resolution → mise installs glob
ol=$(command -v openlogs || true)
if [ -z "$ol" ] && command -v mise >/dev/null 2>&1; then
ol=$(mise which openlogs 2>/dev/null || true)
fi
if [ -z "$ol" ]; then
for cand in "$HOME"/.local/share/mise/installs/node/*/bin/openlogs; do
[ -x "$cand" ] && { ol=$cand; break; }
done
fi
if [ -n "$ol" ] && [ -x "$ol" ]; then
wrapper=$ol
else
echo "pj: openlogs not found, running unwrapped" >&2
fi
fi
if [ -n "$wrapper" ]; then
echo "→ $wrapper $pm run $script $*" >&2
exec "$wrapper" "$pm" run "$script" "${extra[@]}"
else
echo "→ $pm run $script $*" >&2
exec "$pm" run "$script" "${extra[@]}"
fi