Initial commit from B2 archive

This commit is contained in:
Ryan T. Murphy
2026-01-22 18:46:03 -05:00
commit c79c5eef4a
5 changed files with 994 additions and 0 deletions

605
cw.sh Normal file
View File

@@ -0,0 +1,605 @@
#!/bin/bash
# Project Selector for Claude Code
# Usage: cw [args passed to claude]
# Source this file in your .bashrc
cw() {
# Dependency checks
if ! command -v fzf &>/dev/null; then
echo "Error: fzf is required but not installed" >&2
return 1
fi
local claude_bin
claude_bin=$(command -v claude 2>/dev/null)
if [ -z "$claude_bin" ]; then
echo "Error: claude is required but not installed" >&2
echo "Install via npm: sudo npm install -g @anthropic-ai/claude-code" >&2
return 1
fi
# Session/load check - may block or show info
_cw_session_check || return 0
local dir=""
local category=""
# Outer loop to allow returning to category selection
while true; do
# Category selection
category=$(printf '%s\n' \
"💼 Sales" \
"📋 Admin" \
"🎮 Fun" \
"🚀 Pre-Revenue" \
"👥 Clients" \
"🏠 Personal" \
"🔧 Infrastructure" \
"🔍 All Repos" \
"📂 Manual Selection" \
| fzf --height=14 --border --header="What are we working on today?")
[ -z "$category" ] && return 0
# Helper to build category menu with [CHANGE CATEGORY] and [NEW FOLDER]
_cw_category_menu() {
local base_path="$1"
shift
local items=("$@")
printf '%s\n' "[CHANGE CATEGORY]" "[NEW FOLDER]"
if [ ${#items[@]} -gt 0 ]; then
printf '%s\n' "${items[@]}"
fi
}
# Map category to folder(s) and show projects
case "$category" in
"💼 Sales")
local -a sales_dirs=()
while IFS= read -r d; do
[ -n "$d" ] && sales_dirs+=("${d%/}")
done < <(ls -d /opt/clients/client-upfrontops/*/ 2>/dev/null)
dir=$(_cw_category_menu "/opt/clients/client-upfrontops" "${sales_dirs[@]}" \
| fzf --height=15 --border --header="Select sales area (or NEW FOLDER)")
[ "$dir" = "[NEW FOLDER]" ] && { _cw_new_folder "/opt/clients/client-upfrontops" && dir="$_CW_NEW_DIR" || continue; }
;;
"📋 Admin")
# Admin includes personal + docker-vpn
local -a admin_dirs=()
while IFS= read -r d; do
[ -n "$d" ] && admin_dirs+=("${d%/}")
done < <(ls -d /opt/personal/*/ 2>/dev/null)
admin_dirs+=("/opt/docker-vpn")
dir=$(_cw_category_menu "/opt/personal" "${admin_dirs[@]}" \
| fzf --height=15 --border --header="Select admin area (or NEW FOLDER)")
[ "$dir" = "[NEW FOLDER]" ] && { _cw_new_folder "/opt/personal" && dir="$_CW_NEW_DIR" || continue; }
;;
"🎮 Fun")
local -a fun_dirs=()
while IFS= read -r d; do
[ -n "$d" ] && fun_dirs+=("${d%/}")
done < <(ls -d /opt/fun/*/ 2>/dev/null)
if [ ${#fun_dirs[@]} -eq 0 ]; then
echo "No projects in /opt/fun/ yet"
fi
dir=$(_cw_category_menu "/opt/fun" "${fun_dirs[@]}" \
| fzf --height=15 --border --header="Select fun project (or NEW FOLDER)")
[ "$dir" = "[NEW FOLDER]" ] && { _cw_new_folder "/opt/fun" && dir="$_CW_NEW_DIR" || continue; }
;;
"🚀 Pre-Revenue")
local -a prerev_dirs=()
while IFS= read -r d; do
[ -n "$d" ] && prerev_dirs+=("${d%/}")
done < <(ls -d /opt/pre-revenue/*/ 2>/dev/null)
dir=$(_cw_category_menu "/opt/pre-revenue" "${prerev_dirs[@]}" \
| fzf --height=15 --border --header="Select pre-revenue project (or NEW FOLDER)")
[ "$dir" = "[NEW FOLDER]" ] && { _cw_new_folder "/opt/pre-revenue" && dir="$_CW_NEW_DIR" || continue; }
;;
"👥 Clients")
local -a client_dirs=()
while IFS= read -r d; do
[ -n "$d" ] && client_dirs+=("${d%/}")
done < <(ls -d /opt/clients/*/ 2>/dev/null)
dir=$(_cw_category_menu "/opt/clients" "${client_dirs[@]}" \
| fzf --height=15 --border --header="Select client (or NEW FOLDER)")
[ "$dir" = "[NEW FOLDER]" ] && { _cw_new_folder "/opt/clients" && dir="$_CW_NEW_DIR" || continue; }
;;
"🏠 Personal")
local -a personal_dirs=()
while IFS= read -r d; do
[ -n "$d" ] && [[ "$d" != *"/legal/"* ]] && personal_dirs+=("${d%/}")
done < <(ls -d /opt/personal/*/ 2>/dev/null)
dir=$(_cw_category_menu "/opt/personal" "${personal_dirs[@]}" \
| fzf --height=15 --border --header="Select personal project (or NEW FOLDER)")
[ "$dir" = "[NEW FOLDER]" ] && { _cw_new_folder "/opt/personal" && dir="$_CW_NEW_DIR" || continue; }
;;
"🔧 Infrastructure")
# Infrastructure scans /opt/infra + known infrastructure dirs
local -a infra_dirs=()
while IFS= read -r d; do
[ -n "$d" ] && infra_dirs+=("${d%/}")
done < <(ls -d /opt/infra/*/ 2>/dev/null)
# Add other infra locations
infra_dirs+=("/opt/docker-vpn" "/opt/marketplace" "/opt/tools")
[ -d "/opt/docker-parlay-edge" ] && infra_dirs+=("/opt/docker-parlay-edge")
dir=$(_cw_category_menu "/opt/infra" "${infra_dirs[@]}" \
| fzf --height=15 --border --header="Select infrastructure (or NEW FOLDER)")
[ "$dir" = "[NEW FOLDER]" ] && { _cw_new_folder "/opt/infra" && dir="$_CW_NEW_DIR" || continue; }
;;
"🔍 All Repos")
local -a repo_dirs=()
while IFS= read -r d; do
[ -n "$d" ] && repo_dirs+=("$d")
done < <(find /opt -name ".git" -type d 2>/dev/null | sed 's/\/.git$//' | sort)
dir=$(printf '%s\n' "[CHANGE CATEGORY]" "${repo_dirs[@]}" \
| fzf --height=40% --border --header="All git repos (type to search)")
;;
"📂 Manual Selection")
dir="/opt"
;;
esac
# Handle [CHANGE CATEGORY] or cancel
[ -z "$dir" ] && return 0
[ "$dir" = "[CHANGE CATEGORY]" ] && continue
dir="${dir%/}" # Remove trailing slash
# Manual drill-down mode
while true; do
# Build menu: [START HERE], [NEW FOLDER], [GO BACK] if not at /opt, [CHANGE CATEGORY], then subdirs
local -a menu_items=("[START HERE]" "[NEW FOLDER]")
[ "$dir" != "/opt" ] && menu_items+=("[GO BACK]")
menu_items+=("[CHANGE CATEGORY]")
# Only show subdirs if NOT a git repo (don't drill into repos)
if [ ! -d "$dir/.git" ]; then
local -a subdirs=()
while IFS= read -r subdir; do
[ -n "$subdir" ] && [ "$subdir" != "containerd" ] && subdirs+=("$subdir")
done < <(ls -d "$dir"/*/ 2>/dev/null | xargs -r -n1 basename 2>/dev/null | sort)
menu_items+=("${subdirs[@]}")
fi
local choice
choice=$(printf '%s\n' "${menu_items[@]}" | fzf --prompt="$dir/ " --height=40% --border --header="Select folder, START HERE, or NEW FOLDER")
# Cancelled
[ -z "$choice" ] && return 0
# Go back one level
if [ "$choice" = "[GO BACK]" ]; then
dir=$(dirname "$dir")
continue
fi
# Return to category selection
if [ "$choice" = "[CHANGE CATEGORY]" ]; then
break
fi
# Create new folder at current level
if [ "$choice" = "[NEW FOLDER]" ]; then
if _cw_new_folder "$dir"; then
dir="$_CW_NEW_DIR"
fi
continue
fi
# Selected current dir (or auto-selected git repo)
if [ "$choice" = "[START HERE]" ]; then
if ! cd "$dir" 2>/dev/null; then
echo "Error: Cannot access $dir" >&2
return 1
fi
# Check for CLAUDE.md
if [ -f "CLAUDE.md" ]; then
# CLAUDE.md exists - just offer to manage skills if no .claude/skills
if [ ! -d ".claude/skills" ]; then
echo ""
read -p "Add skills to this project? [y/N] " yn
if [[ "$yn" =~ ^[Yy]$ ]]; then
_cw_link_skills "$(basename "$dir")"
fi
fi
else
_cw_create_claude_md "$dir"
fi
# Check git repo status
local git_root
git_root=$(git rev-parse --show-toplevel 2>/dev/null)
if [ -z "$git_root" ]; then
echo ""
echo "Not inside a git repo"
read -p "Initialize git repo here? [y/N] " yn
if [[ "$yn" =~ ^[Yy]$ ]]; then
git init
echo "Initialized git repo"
fi
elif [ "$git_root" != "$dir" ]; then
echo ""
echo "Inside repo: $git_root"
fi
# Re-check session/load right before launch (prevents race condition)
_cw_session_check || return 0
"$claude_bin" "$@"
return 0
fi
# Drill down into selected subdirectory
dir="$dir/$choice"
done
done # End outer category loop
}
# Helper: Get 5-min load average as percentage of CPU capacity
_cw_get_load_percent() {
local load cpus
load=$(awk '{print $2}' /proc/loadavg)
cpus=$(nproc)
awk "BEGIN {printf \"%.0f\", ($load / $cpus) * 100}"
}
# Helper: Get running Claude sessions with metadata
# Output format per line: PID|CPU%|ELAPSED|CWD|ORPHAN
_cw_get_claude_sessions() {
local pid cpu elapsed tty cwd ppid is_orphan
while read -r pid cpu elapsed tty _; do
[ -z "$pid" ] && continue
# Get working directory
cwd=$(readlink -f "/proc/$pid/cwd" 2>/dev/null) || continue
# Check if orphan (no TTY or parent is init)
is_orphan=""
if [ "$tty" = "?" ]; then
is_orphan="ORPHAN"
else
ppid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
[ "$ppid" = "1" ] && is_orphan="ORPHAN"
fi
echo "$pid|$cpu|$elapsed|$cwd|$is_orphan"
done < <(ps aux 2>/dev/null | awk '/[c]laude/ && !/awk|grep|ripgrep/ {print $2, $3, $10, $7}')
}
# Helper: Format session for display
# Input: PID|CPU%|ELAPSED|CWD|ORPHAN
# Output: /path/to/project (ORPHAN) (1h 02m, 47% CPU)
_cw_format_session() {
local line="$1"
local pid cpu elapsed cwd orphan
IFS='|' read -r pid cpu elapsed cwd orphan <<< "$line"
# Format elapsed time (from [[DD-]HH:]MM:SS to human readable)
local formatted_time=""
if [[ "$elapsed" =~ ^([0-9]+)-([0-9]+):([0-9]+):([0-9]+)$ ]]; then
formatted_time="${BASH_REMATCH[1]}d ${BASH_REMATCH[2]}h"
elif [[ "$elapsed" =~ ^([0-9]+):([0-9]+):([0-9]+)$ ]]; then
local hours="${BASH_REMATCH[1]}"
local mins="${BASH_REMATCH[2]}"
if [ "$hours" -gt 0 ]; then
formatted_time="${hours}h ${mins}m"
else
formatted_time="${mins}m"
fi
elif [[ "$elapsed" =~ ^([0-9]+):([0-9]+)$ ]]; then
formatted_time="${BASH_REMATCH[1]}m"
else
formatted_time="$elapsed"
fi
# Build output
local output="$cwd"
[ -n "$orphan" ] && output="$output (ORPHAN)"
output="$output ($formatted_time, ${cpu}% CPU)"
echo "$output"
}
# Helper: Kill a Claude session gracefully
# Returns 0 on success, 1 on failure
_cw_kill_session() {
local pid="$1"
local cwd="$2"
echo "Stopping $cwd (PID $pid)..."
# Send SIGTERM
kill -TERM "$pid" 2>/dev/null
# Wait up to 5 seconds for graceful termination
local waited=0
while [ $waited -lt 5 ]; do
if ! kill -0 "$pid" 2>/dev/null; then
echo "Terminated gracefully."
return 0
fi
sleep 1
((waited++))
done
# Still alive, force kill
echo "Process didn't stop, forcing..."
kill -KILL "$pid" 2>/dev/null
sleep 0.5
if ! kill -0 "$pid" 2>/dev/null; then
echo "Terminated forcefully."
return 0
else
echo "Error: Failed to kill process $pid" >&2
return 1
fi
}
# Helper: Session check orchestrator
# Returns 0 to proceed, 1 to abort
_cw_session_check() {
local load_percent session_count
local -a sessions=()
local -a orphans=()
local -a formatted=()
# Gather data
load_percent=$(_cw_get_load_percent)
while IFS= read -r line; do
[ -n "$line" ] && sessions+=("$line")
done < <(_cw_get_claude_sessions)
session_count=${#sessions[@]}
# No sessions running - skip display entirely
[ "$session_count" -eq 0 ] && return 0
# Auto-kill orphans first
local orphan_count=0
local -a live_sessions=()
for session in "${sessions[@]}"; do
local pid cwd orphan
IFS='|' read -r pid _ _ cwd orphan <<< "$session"
if [ "$orphan" = "ORPHAN" ]; then
echo "Killing orphaned session: $cwd (PID $pid)"
kill -KILL "$pid" 2>/dev/null
((orphan_count++))
else
live_sessions+=("$session")
fi
done
if [ "$orphan_count" -gt 0 ]; then
echo "Cleaned up $orphan_count orphaned session(s)"
echo ""
fi
# Update sessions to only live ones
sessions=("${live_sessions[@]}")
session_count=${#sessions[@]}
# No live sessions remaining - skip display
[ "$session_count" -eq 0 ] && return 0
# Format sessions for display
for session in "${sessions[@]}"; do
formatted+=("$(_cw_format_session "$session")")
done
# Determine tier
local hard_block=false
if [ "$session_count" -ge 4 ] || [ "$load_percent" -ge 60 ]; then
hard_block=true
fi
if [ "$hard_block" = true ]; then
# HARD BLOCK - must kill sessions until below threshold
# Need to get to <4 sessions AND <60% load
local sessions_to_kill=0
if [ "$session_count" -ge 4 ]; then
sessions_to_kill=$((session_count - 3)) # Get down to 3
fi
# If load is the issue, need to kill at least 1
if [ "$load_percent" -ge 60 ] && [ "$sessions_to_kill" -lt 1 ]; then
sessions_to_kill=1
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " Cannot start new Claude session"
echo " Load: ${load_percent}% (5-min avg) | Sessions: $session_count running"
if [ "$sessions_to_kill" -gt 1 ]; then
echo " Must kill $sessions_to_kill sessions to continue"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
local killed=0
while [ "$killed" -lt "$sessions_to_kill" ]; do
local remaining=$((sessions_to_kill - killed))
local header="Select a session to kill ($remaining more required, Esc to cancel)"
[ "$remaining" -eq 1 ] && header="Select a session to kill (Esc to cancel)"
local choice
choice=$(printf '%s\n' "${formatted[@]}" | fzf --height=15 --border --header="$header")
[ -z "$choice" ] && return 1
# Find the matching session to get its PID
local idx=0
for f in "${formatted[@]}"; do
if [ "$f" = "$choice" ]; then
local session_data="${sessions[$idx]}"
local pid cwd
IFS='|' read -r pid _ _ cwd _ <<< "$session_data"
echo ""
if _cw_kill_session "$pid" "$cwd"; then
((killed++))
# Remove from arrays
unset 'sessions[idx]'
unset 'formatted[idx]'
sessions=("${sessions[@]}")
formatted=("${formatted[@]}")
else
echo "Try selecting another session."
fi
break
fi
((idx++))
done
done
echo ""
echo "Proceeding to project selection..."
sleep 1
return 0
else
# INFO ONLY - show sessions and continue
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " Load: ${load_percent}% (5-min avg) | Sessions: $session_count running"
for f in "${formatted[@]}"; do
echo " $f"
done
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
return 0
fi
}
# Helper: Create new folder
# Sets $_CW_NEW_DIR on success
_cw_new_folder() {
local parent="$1"
_CW_NEW_DIR=""
# Ensure parent exists
if [ ! -d "$parent" ]; then
echo "Creating parent directory: $parent"
mkdir -p "$parent" || { echo "Error: Cannot create $parent" >&2; return 1; }
fi
echo ""
read -p "New folder name in $parent/: " folder_name
# Validate folder name
if [ -z "$folder_name" ]; then
echo "Cancelled"
return 1
fi
# Remove problematic characters
folder_name=$(echo "$folder_name" | tr -cd 'a-zA-Z0-9_-')
if [ -z "$folder_name" ]; then
echo "Error: Invalid folder name" >&2
return 1
fi
local new_path="$parent/$folder_name"
if [ -d "$new_path" ]; then
echo "Folder already exists: $new_path"
_CW_NEW_DIR="$new_path"
return 0
fi
if mkdir -p "$new_path"; then
echo "Created: $new_path"
_CW_NEW_DIR="$new_path"
return 0
else
echo "Error: Cannot create $new_path" >&2
return 1
fi
}
# Helper: Link skills using fzf multi-select
_cw_link_skills() {
local project_name="$1"
if [ ! -d "/opt/marketplace/skills" ]; then
echo "No skills directory found at /opt/marketplace/skills"
return 1
fi
echo "Select skills (Tab to toggle, Enter to confirm):"
local -a selected_skills=()
while IFS= read -r skill; do
[ -n "$skill" ] && selected_skills+=("$skill")
done < <(ls /opt/marketplace/skills/ 2>/dev/null | fzf --multi --height=50% --border --header="Select skills for $project_name (Tab=toggle, Enter=done)")
if [ ${#selected_skills[@]} -eq 0 ]; then
return 0
fi
mkdir -p .claude/skills
for skill in "${selected_skills[@]}"; do
if [ -d "/opt/marketplace/skills/$skill" ]; then
ln -sf "/opt/marketplace/skills/$skill" ".claude/skills/$skill"
echo "Linked skill: $skill"
else
echo "Skill not found: $skill" >&2
fi
done
}
# Helper: Create CLAUDE.md with safe input handling
_cw_create_claude_md() {
local dir="$1"
local project_name
project_name=$(basename "$dir")
echo ""
echo "No CLAUDE.md found in $dir"
read -p "Create CLAUDE.md? [y/N] " yn
if [[ ! "$yn" =~ ^[Yy]$ ]]; then
return 0
fi
echo ""
read -p "Project description (1-2 sentences): " proj_desc
echo ""
read -p "Common commands (or press Enter to skip): " proj_cmds
echo ""
# Create CLAUDE.md safely with printf (no variable expansion issues)
{
printf '%s\n' "# $project_name"
printf '\n'
printf '%s\n' "## Project Overview"
printf '%s\n' "$proj_desc"
printf '\n'
printf '%s\n' "## Quick Reference"
printf '%s\n' "- **Timezone:** EST (America/New_York)"
if [ -n "$proj_cmds" ]; then
printf '\n'
printf '%s\n' "## Common Commands"
printf '%s\n' '```bash'
printf '%s\n' "$proj_cmds"
printf '%s\n' '```'
fi
} > CLAUDE.md
echo "Created CLAUDE.md"
# Link skills
_cw_link_skills "$project_name"
}