-
-
Save nhatnx/7eb71224452d8ee6f8a5fbf6249809ed to your computer and use it in GitHub Desktop.
Cleaned up, verified and translated version of weidwonder/claude-desktop-multi-instance
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Claude Desktop Multi-Instance Launcher | |
| # | |
| # Uses Electron's --user-data-dir flag to run fully isolated instances. | |
| # Each instance gets its own data directory (config, VM bundles, SDK, sessions). | |
| # | |
| # Need to make the helper executable | |
| # chmod +x ./claude_quick.sh | |
| # | |
| # Usage: | |
| # ./claude_quick.sh # Show interactive menu | |
| # ./claude_quick.sh <instance> # Launch named instance | |
| # ./claude_quick.sh list # List all instances | |
| # ./claude_quick.sh delete <name> # Delete an instance | |
| # ./claude_quick.sh wrapper <name> # Create app wrapper for Dock/Launchpad | |
| # ./claude_quick.sh fix # Regenerate wrapper launcher scripts | |
| # ./claude_quick.sh diagnose # Diagnose problems | |
| INSTANCES_BASE="$HOME/.claude-instances" | |
| # ==================== Functions ==================== | |
| copy_claude_icon() { | |
| local target_dir="$1" | |
| local icon=$(find "/Applications/Claude.app" -name "*.icns" -type f 2>/dev/null | head -n 1) | |
| if [ -n "$icon" ]; then | |
| cp "$icon" "$target_dir/claude-icon.icns" | |
| echo "✅ Copied icon: $(basename "$icon")" | |
| else | |
| echo "⚠️ No icon found, wrapper will use default macOS icon" | |
| fi | |
| } | |
| list_instance_names() { | |
| ls -1 "$INSTANCES_BASE" 2>/dev/null | |
| } | |
| # List all instance names including the virtual "default" instance. | |
| list_all_instance_names() { | |
| echo "default" | |
| list_instance_names | |
| } | |
| # Find the .app wrapper for a given instance by checking its config.sh. | |
| find_wrapper_for_instance() { | |
| local target="$1" | |
| for app in /Applications/*.app; do | |
| local config="$app/Contents/MacOS/config.sh" | |
| [ ! -f "$config" ] && continue | |
| local name=$(grep "^INSTANCE_NAME=" "$config" 2>/dev/null | head -1 | sed 's/^INSTANCE_NAME=//') | |
| if [ "$name" = "$target" ]; then | |
| echo "$app" | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| # List all .app bundles that have a config.sh (i.e., Claude instance wrappers). | |
| find_all_wrappers() { | |
| for app in /Applications/*.app; do | |
| [ -f "$app/Contents/MacOS/config.sh" ] && echo "$app" | |
| done | |
| } | |
| # Extract the instance name from a wrapper's config.sh. | |
| get_wrapper_instance_name() { | |
| local config="$1/Contents/MacOS/config.sh" | |
| grep "^INSTANCE_NAME=" "$config" 2>/dev/null | head -1 | sed 's/^INSTANCE_NAME=//' | |
| } | |
| list_instances() { | |
| echo "Claude Desktop Instances" | |
| echo "========================" | |
| echo "" | |
| # Default instance (uses Claude's built-in data directory) | |
| local wrapper_status="no wrapper" | |
| find_wrapper_for_instance "default" &>/dev/null && wrapper_status="has wrapper" | |
| echo " - default ($wrapper_status, built-in)" | |
| local count=0 | |
| for dir in "$INSTANCES_BASE"/*/; do | |
| [ ! -d "$dir" ] && continue | |
| local name=$(basename "$dir") | |
| local wrapper_status="no wrapper" | |
| find_wrapper_for_instance "$name" &>/dev/null && wrapper_status="has wrapper" | |
| echo " - $name ($wrapper_status)" | |
| count=$((count + 1)) | |
| done | |
| echo "" | |
| } | |
| diagnose() { | |
| echo "🔍 Claude Desktop Diagnostics" | |
| echo "==============================" | |
| echo "" | |
| if [ ! -d "/Applications/Claude.app" ]; then | |
| echo "❌ Claude.app not found in /Applications" | |
| echo " Download from https://claude.ai/download" | |
| return | |
| fi | |
| echo "✅ Claude.app installed" | |
| echo "" | |
| echo "App wrappers:" | |
| local found=false | |
| while IFS= read -r app; do | |
| found=true | |
| local instance=$(get_wrapper_instance_name "$app") | |
| local display=$(basename "$app" .app) | |
| echo " 📱 $display → instance: $instance" | |
| done < <(find_all_wrappers) | |
| $found || echo " (none)" | |
| echo "" | |
| echo "Instance directories:" | |
| if [ -d "$INSTANCES_BASE" ]; then | |
| list_instance_names | sed 's/^/ - /' || echo " (none)" | |
| else | |
| echo " (no instances directory)" | |
| fi | |
| } | |
| # Write the static claude-launcher script into a wrapper. | |
| # Can be safely regenerated at any time — config.sh holds the instance name. | |
| write_launcher_script() { | |
| local wrapper_path="$1" | |
| local script_path="$wrapper_path/Contents/MacOS/claude-launcher" | |
| local main_script=$(which claude_quick.sh 2>/dev/null) | |
| if [ -z "$main_script" ]; then | |
| echo "❌ 'claude_quick.sh' not found in PATH" | |
| return 1 | |
| fi | |
| cat > "$script_path" << LAUNCHER_EOF | |
| #!/bin/bash | |
| source "\$(dirname "\$0")/config.sh" | |
| exec "$main_script" "\$INSTANCE_NAME" | |
| LAUNCHER_EOF | |
| chmod +x "$script_path" | |
| } | |
| fix_wrappers() { | |
| echo "🔧 Fixing Claude Desktop wrappers" | |
| echo "==================================" | |
| local found=false | |
| while IFS= read -r app; do | |
| found=true | |
| local instance=$(get_wrapper_instance_name "$app") | |
| echo "🔨 Repairing: $(basename "$app") (instance: $instance)" | |
| if write_launcher_script "$app"; then | |
| echo " ✅ Regenerated launcher script" | |
| else | |
| echo " ❌ Failed to regenerate launcher" | |
| fi | |
| if [ ! -f "$app/Contents/Resources/claude-icon.icns" ]; then | |
| copy_claude_icon "$app/Contents/Resources" | |
| fi | |
| done < <(find_all_wrappers) | |
| $found || echo "No wrappers found to fix." | |
| echo "✅ Done" | |
| } | |
| delete_instance() { | |
| local name="$1" | |
| if [ -z "$name" ]; then | |
| echo "Available instances:" | |
| list_instance_names | sed 's/^/ - /' | |
| echo "" | |
| read -p "Instance name to delete: " name | |
| fi | |
| if [ -z "$name" ]; then | |
| echo "❌ Invalid instance name" | |
| return 1 | |
| fi | |
| if is_default_instance "$name"; then | |
| echo "❌ Cannot delete the default instance" | |
| return 1 | |
| fi | |
| if [ ! -d "$INSTANCES_BASE/$name" ]; then | |
| echo "❌ Instance '$name' does not exist" | |
| return 1 | |
| fi | |
| local wrapper=$(find_wrapper_for_instance "$name") | |
| echo "⚠️ This will delete:" | |
| echo " - Data: $INSTANCES_BASE/$name" | |
| [ -n "$wrapper" ] && echo " - Wrapper: $wrapper" | |
| echo "" | |
| read -p "Type 'yes' to confirm: " confirm | |
| if [ "$confirm" != "yes" ]; then | |
| echo "Cancelled." | |
| return 0 | |
| fi | |
| rm -rf "$INSTANCES_BASE/$name" | |
| [ -n "$wrapper" ] && rm -rf "$wrapper" | |
| echo "✅ Instance '$name' deleted" | |
| } | |
| create_app_wrapper() { | |
| local instance_name="$1" | |
| local display_name="$2" | |
| local wrapper_path="/Applications/Claude $display_name.app" | |
| if [ -d "$wrapper_path" ]; then | |
| read -p "⚠️ Wrapper already exists. Overwrite? (y/N): " overwrite | |
| [ "$overwrite" != "y" ] && [ "$overwrite" != "Y" ] && return | |
| rm -rf "$wrapper_path" | |
| fi | |
| mkdir -p "$wrapper_path/Contents/MacOS" | |
| mkdir -p "$wrapper_path/Contents/Resources" | |
| cat > "$wrapper_path/Contents/Info.plist" << EOF | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>CFBundleExecutable</key> | |
| <string>claude-launcher</string> | |
| <key>CFBundleIdentifier</key> | |
| <string>com.anthropic.claude.$instance_name</string> | |
| <key>CFBundleName</key> | |
| <string>Claude $display_name</string> | |
| <key>CFBundleDisplayName</key> | |
| <string>Claude $display_name</string> | |
| <key>CFBundleVersion</key> | |
| <string>1.0</string> | |
| <key>CFBundleShortVersionString</key> | |
| <string>1.0</string> | |
| <key>CFBundlePackageType</key> | |
| <string>APPL</string> | |
| <key>CFBundleInfoDictionaryVersion</key> | |
| <string>6.0</string> | |
| <key>LSMinimumSystemVersion</key> | |
| <string>11.0</string> | |
| <key>CFBundleIconFile</key> | |
| <string>claude-icon</string> | |
| <key>NSHighResolutionCapable</key> | |
| <true/> | |
| </dict> | |
| </plist> | |
| EOF | |
| cat > "$wrapper_path/Contents/MacOS/config.sh" << CONFIG_EOF | |
| INSTANCE_NAME=$instance_name | |
| CONFIG_EOF | |
| write_launcher_script "$wrapper_path" | |
| copy_claude_icon "$wrapper_path/Contents/Resources" | |
| echo "✅ Wrapper created: $wrapper_path" | |
| echo " Search 'Claude $display_name' in Launchpad or drag to Dock." | |
| } | |
| prompt_wrapper_creation() { | |
| local instance_name="$1" | |
| if [ -z "$instance_name" ]; then | |
| echo "Available instances:" | |
| list_all_instance_names | sed 's/^/ - /' | |
| echo "" | |
| read -p "Create wrapper for which instance: " instance_name | |
| fi | |
| if [ -z "$instance_name" ]; then | |
| echo "❌ Invalid instance name" | |
| return 1 | |
| fi | |
| if ! is_default_instance "$instance_name" && [ ! -d "$INSTANCES_BASE/$instance_name" ]; then | |
| echo "❌ Instance '$instance_name' does not exist" | |
| return 1 | |
| fi | |
| local default_suffix=$(echo "$instance_name" | awk '{print toupper(substr($0,1,1)) substr($0,2)}') | |
| read -p "App name suffix (will show as 'Claude <suffix>') [$default_suffix]: " suffix | |
| [ -z "$suffix" ] && suffix="$default_suffix" | |
| create_app_wrapper "$instance_name" "$suffix" | |
| } | |
| is_default_instance() { | |
| [ "$1" = "default" ] | |
| } | |
| launch_instance() { | |
| local instance_name="$1" | |
| echo "🚀 Launching instance: $instance_name" | |
| if is_default_instance "$instance_name"; then | |
| open -n -a "/Applications/Claude.app" | |
| echo "✅ Claude Desktop launched (default)" | |
| else | |
| local instance_dir="$INSTANCES_BASE/$instance_name" | |
| mkdir -p "$instance_dir" | |
| open -n -a "/Applications/Claude.app" --args --user-data-dir="$instance_dir" | |
| echo "✅ Claude Desktop launched" | |
| echo "📂 Data: $instance_dir" | |
| fi | |
| if ! find_wrapper_for_instance "$instance_name" &>/dev/null; then | |
| echo "💡 Run '$0 wrapper $instance_name' to create a Dock shortcut." | |
| fi | |
| } | |
| # ==================== Main ==================== | |
| echo "======================================" | |
| echo " Claude Desktop Multi-Instance Launcher" | |
| echo "======================================" | |
| if [ ! -d "/Applications/Claude.app" ]; then | |
| echo "❌ Claude Desktop not found. Install from https://claude.ai/download" | |
| exit 1 | |
| fi | |
| # Handle subcommands | |
| case "$1" in | |
| list) list_instances; exit 0 ;; | |
| delete) delete_instance "$2"; exit $? ;; | |
| wrapper) prompt_wrapper_creation "$2"; exit $? ;; | |
| fix) fix_wrappers; exit 0 ;; | |
| diagnose) diagnose; exit 0 ;; | |
| esac | |
| # Direct instance launch | |
| if [ -n "$1" ]; then | |
| launch_instance "$1" | |
| exit 0 | |
| fi | |
| # Interactive menu | |
| echo "" | |
| echo "1. Launch default instance" | |
| echo "2. Select existing instance" | |
| echo "3. Create new instance" | |
| echo "4. Delete instance" | |
| echo "5. Create app wrapper" | |
| echo "6. Diagnose problems" | |
| echo "7. Fix wrappers" | |
| echo "" | |
| read -p "Select (1-7): " choice | |
| case $choice in | |
| 1) launch_instance "default" ;; | |
| 2) | |
| list_all_instance_names | sed 's/^/ - /' | |
| echo "" | |
| read -p "Instance name: " name | |
| [ -n "$name" ] && launch_instance "$name" || echo "❌ No name provided" | |
| ;; | |
| 3) | |
| read -p "New instance name: " name | |
| if [ -z "$name" ]; then | |
| echo "❌ No name provided" | |
| elif is_default_instance "$name"; then | |
| echo "❌ 'default' already exists as the built-in instance" | |
| else | |
| launch_instance "$name" | |
| fi | |
| ;; | |
| 4) delete_instance ;; | |
| 5) prompt_wrapper_creation ;; | |
| 6) diagnose ;; | |
| 7) fix_wrappers ;; | |
| *) echo "Invalid selection." ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment