Initial commit from B2 archive
This commit is contained in:
605
cw.sh
Normal file
605
cw.sh
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user