Last active
January 19, 2026 22:45
-
-
Save jeffreyschultz/3530806e5641d539d08ffa687098386a to your computer and use it in GitHub Desktop.
VSCode launcher that manages VSCode and VSCode Insiders profiles with isolated user data directories
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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # VSCode Profile Launcher | |
| # Manages VSCode and VSCode Insiders profiles with isolated user data directories | |
| # Detect if we're in WSL and set up paths accordingly | |
| if [[ -n "$WSL_DISTRO_NAME" ]]; then | |
| # In WSL, store profiles in Windows filesystem to avoid locking issues | |
| WINDOWS_USERPROFILE="$(wslpath "$(cmd.exe /c 'echo %USERPROFILE%' 2>/dev/null | tr -d '\r')")" | |
| STABLE_BASE_DIR="$WINDOWS_USERPROFILE/.vscode-profiles/stable" | |
| INSIDERS_BASE_DIR="$WINDOWS_USERPROFILE/.vscode-profiles/insiders" | |
| else | |
| # Native Linux or other environments | |
| STABLE_BASE_DIR="$HOME/.vscode/stable" | |
| INSIDERS_BASE_DIR="$HOME/.vscode/insiders" | |
| fi | |
| STABLE_CMD="code" | |
| INSIDERS_CMD="code-insiders" | |
| # Colors for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Color | |
| print_error() { | |
| echo -e "${RED}Error: $1${NC}" >&2 | |
| } | |
| print_success() { | |
| echo -e "${GREEN}$1${NC}" | |
| } | |
| print_info() { | |
| echo -e "${BLUE}$1${NC}" | |
| } | |
| print_warning() { | |
| echo -e "${YELLOW}$1${NC}" | |
| } | |
| show_help() { | |
| cat <<EOF | |
| VSCode Profile Launcher | |
| Usage: | |
| vscode [OPTIONS] <profile-name> [vscode-args...] | |
| vscode <command> [args...] | |
| Launch VSCode with a profile: | |
| vscode <profile-name> Launch VSCode stable with the specified profile | |
| vscode -i <profile-name> Launch VSCode Insiders with the specified profile | |
| vscode <profile-name> [args...] Pass additional arguments to VSCode | |
| Commands: | |
| list, ls List all profiles (stable and insiders) | |
| list-stable List stable profiles only | |
| list-insiders List insiders profiles only | |
| remove, rm <profile-name> Remove a stable profile | |
| remove-insiders <profile-name> Remove an insiders profile | |
| path <profile-name> Show path to stable profile directory | |
| path-insiders <profile-name> Show path to insiders profile directory | |
| clean Remove empty profile directories | |
| help, -h, --help Show this help message | |
| Options: | |
| -i, --insiders Use VSCode Insiders instead of stable | |
| -n, --new Force creation of new profile (create dirs if needed) | |
| Examples: | |
| vscode work Launch VSCode with 'work' profile | |
| vscode -i personal Launch VSCode Insiders with 'personal' profile | |
| vscode work /path/to/project Open project with 'work' profile | |
| vscode list List all profiles | |
| vscode rm old-project Remove 'old-project' profile from stable | |
| vscode path work Show path to 'work' profile directory | |
| Profile directories: | |
| Stable: $STABLE_BASE_DIR/{name} | |
| Insiders: $INSIDERS_BASE_DIR/{name} | |
| EOF | |
| } | |
| check_vscode_installed() { | |
| local cmd=$1 | |
| local variant=$2 | |
| if ! command -v "$cmd" &> /dev/null; then | |
| print_error "$variant is not installed or not in PATH" | |
| exit 1 | |
| fi | |
| } | |
| get_vscode_executable() { | |
| local cmd=$1 | |
| local cmd_path | |
| cmd_path=$(which "$cmd" 2>/dev/null) | |
| if [[ -z "$cmd_path" ]]; then | |
| echo "" | |
| return | |
| fi | |
| # Check if we're in WSL and need to find the Windows executable | |
| if [[ -n "$WSL_DISTRO_NAME" ]]; then | |
| # Read the script to find VSCODE_PATH | |
| local vscode_dir | |
| vscode_dir=$(dirname "$(dirname "$cmd_path")") | |
| # Determine the executable name based on stable/insiders | |
| local exe_name="Code.exe" | |
| if [[ "$cmd" == "code-insiders" ]]; then | |
| exe_name="Code - Insiders.exe" | |
| fi | |
| local windows_exe="$vscode_dir/$exe_name" | |
| if [[ -f "$windows_exe" ]]; then | |
| echo "$windows_exe" | |
| return | |
| fi | |
| fi | |
| # Fallback to the command itself | |
| echo "$cmd" | |
| } | |
| get_profile_dir() { | |
| local profile_name=$1 | |
| local use_insiders=$2 | |
| if [[ $use_insiders -eq 1 ]]; then | |
| echo "$INSIDERS_BASE_DIR/$profile_name" | |
| else | |
| echo "$STABLE_BASE_DIR/$profile_name" | |
| fi | |
| } | |
| ensure_profile_dir() { | |
| local profile_dir=$1 | |
| if [[ ! -d "$profile_dir" ]]; then | |
| mkdir -p "$profile_dir" | |
| print_success "Created profile directory: $profile_dir" | |
| fi | |
| } | |
| launch_vscode() { | |
| local profile_name=$1 | |
| local use_insiders=$2 | |
| shift 2 | |
| local extra_args=("$@") | |
| local cmd | |
| local variant | |
| if [[ $use_insiders -eq 1 ]]; then | |
| cmd=$INSIDERS_CMD | |
| variant="VSCode Insiders" | |
| else | |
| cmd=$STABLE_CMD | |
| variant="VSCode" | |
| fi | |
| check_vscode_installed "$cmd" "$variant" | |
| local profile_base_dir | |
| profile_base_dir=$(get_profile_dir "$profile_name" "$use_insiders") | |
| # Profile and server are stored in subdirectories | |
| local profile_dir="$profile_base_dir/profile" | |
| local server_dir="$profile_base_dir/server" | |
| ensure_profile_dir "$profile_dir" | |
| ensure_profile_dir "$server_dir" | |
| # Get the actual executable (handles WSL) | |
| local executable | |
| executable=$(get_vscode_executable "$cmd") | |
| if [[ -z "$executable" ]]; then | |
| print_error "Could not locate $variant executable" | |
| exit 1 | |
| fi | |
| # Set VSCODE_SERVER_HOME to isolate the vscode-server for this profile | |
| export VSCODE_SERVER_HOME="$server_dir" | |
| print_info "Launching $variant with profile: $profile_name" | |
| print_info "User data dir: $profile_dir" | |
| print_info "Server dir: $server_dir" | |
| # Convert path to Windows format if in WSL (profiles may be on Windows filesystem) | |
| local exec_profile_dir="$profile_dir" | |
| local remote_flag=() | |
| if [[ -n "$WSL_DISTRO_NAME" ]] && command -v wslpath &> /dev/null; then | |
| # Convert to Windows path and replace backslashes with forward slashes | |
| # Windows handles forward slashes fine and it avoids escaping issues | |
| exec_profile_dir=$(wslpath -w "$profile_dir" | sed 's|\\\\|/|g') | |
| # Add --remote flag to establish WSL context | |
| remote_flag=("--remote" "wsl+$WSL_DISTRO_NAME") | |
| fi | |
| # Convert any relative paths in extra_args to absolute Linux paths | |
| local resolved_args=() | |
| for arg in "${extra_args[@]}"; do | |
| # Check if arg looks like a path (doesn't start with -) | |
| if [[ ! "$arg" =~ ^- ]] && [[ -e "$arg" || "$arg" =~ / ]]; then | |
| # Convert to absolute path | |
| resolved_args+=("$(realpath -m "$arg")") | |
| else | |
| resolved_args+=("$arg") | |
| fi | |
| done | |
| "$executable" --user-data-dir "$exec_profile_dir" "${remote_flag[@]}" "${resolved_args[@]}" | |
| } | |
| list_profiles() { | |
| local base_dir=$1 | |
| local label=$2 | |
| if [[ ! -d "$base_dir" ]]; then | |
| return | |
| fi | |
| local profiles=() | |
| while IFS= read -r -d '' dir; do | |
| profiles+=("$(basename "$dir")") | |
| done < <(find "$base_dir" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null | sort -z) | |
| if [[ ${#profiles[@]} -gt 0 ]]; then | |
| echo -e "${GREEN}$label:${NC}" | |
| for profile in "${profiles[@]}"; do | |
| echo " - $profile" | |
| done | |
| else | |
| echo -e "${YELLOW}No $label profiles found${NC}" | |
| fi | |
| } | |
| list_all_profiles() { | |
| list_profiles "$STABLE_BASE_DIR" "Stable profiles" | |
| echo | |
| list_profiles "$INSIDERS_BASE_DIR" "Insiders profiles" | |
| } | |
| remove_profile() { | |
| local profile_name=$1 | |
| local use_insiders=$2 | |
| local profile_dir | |
| profile_dir=$(get_profile_dir "$profile_name" "$use_insiders") | |
| if [[ ! -d "$profile_dir" ]]; then | |
| print_error "Profile '$profile_name' does not exist at: $profile_dir" | |
| exit 1 | |
| fi | |
| local variant="stable" | |
| [[ $use_insiders -eq 1 ]] && variant="insiders" | |
| print_warning "This will permanently delete the profile directory and all its contents:" | |
| print_warning " $profile_dir" | |
| read -p "Are you sure? (y/N): " -n 1 -r | |
| echo | |
| if [[ $REPLY =~ ^[Yy]$ ]]; then | |
| rm -rf "$profile_dir" | |
| print_success "Removed $variant profile: $profile_name" | |
| else | |
| print_info "Cancelled" | |
| fi | |
| } | |
| show_profile_path() { | |
| local profile_name=$1 | |
| local use_insiders=$2 | |
| local profile_dir | |
| profile_dir=$(get_profile_dir "$profile_name" "$use_insiders") | |
| if [[ -d "$profile_dir" ]]; then | |
| echo "$profile_dir" | |
| else | |
| print_error "Profile '$profile_name' does not exist" | |
| print_info "Path would be: $profile_dir" | |
| exit 1 | |
| fi | |
| } | |
| clean_empty_profiles() { | |
| local removed_count=0 | |
| for base_dir in "$STABLE_BASE_DIR" "$INSIDERS_BASE_DIR"; do | |
| if [[ ! -d "$base_dir" ]]; then | |
| continue | |
| fi | |
| while IFS= read -r -d '' dir; do | |
| if [[ -z "$(ls -A "$dir")" ]]; then | |
| rmdir "$dir" | |
| print_info "Removed empty directory: $dir" | |
| ((removed_count++)) | |
| fi | |
| done < <(find "$base_dir" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null) | |
| done | |
| if [[ $removed_count -eq 0 ]]; then | |
| print_info "No empty profile directories found" | |
| else | |
| print_success "Removed $removed_count empty profile director(y|ies)" | |
| fi | |
| } | |
| # Main script logic | |
| main() { | |
| if [[ $# -eq 0 ]]; then | |
| show_help | |
| exit 0 | |
| fi | |
| local use_insiders=0 | |
| local force_new=0 | |
| # Parse global options | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| -h|--help|help) | |
| show_help | |
| exit 0 | |
| ;; | |
| list|ls) | |
| list_all_profiles | |
| exit 0 | |
| ;; | |
| list-stable) | |
| list_profiles "$STABLE_BASE_DIR" "Stable profiles" | |
| exit 0 | |
| ;; | |
| list-insiders) | |
| list_profiles "$INSIDERS_BASE_DIR" "Insiders profiles" | |
| exit 0 | |
| ;; | |
| remove|rm) | |
| if [[ $# -lt 2 ]]; then | |
| print_error "Profile name required" | |
| exit 1 | |
| fi | |
| remove_profile "$2" 0 | |
| exit 0 | |
| ;; | |
| remove-insiders) | |
| if [[ $# -lt 2 ]]; then | |
| print_error "Profile name required" | |
| exit 1 | |
| fi | |
| remove_profile "$2" 1 | |
| exit 0 | |
| ;; | |
| path) | |
| if [[ $# -lt 2 ]]; then | |
| print_error "Profile name required" | |
| exit 1 | |
| fi | |
| show_profile_path "$2" 0 | |
| exit 0 | |
| ;; | |
| path-insiders) | |
| if [[ $# -lt 2 ]]; then | |
| print_error "Profile name required" | |
| exit 1 | |
| fi | |
| show_profile_path "$2" 1 | |
| exit 0 | |
| ;; | |
| clean) | |
| clean_empty_profiles | |
| exit 0 | |
| ;; | |
| -i|--insiders) | |
| use_insiders=1 | |
| shift | |
| ;; | |
| -n|--new) | |
| force_new=1 | |
| shift | |
| ;; | |
| -*) | |
| print_error "Unknown option: $1" | |
| echo | |
| show_help | |
| exit 1 | |
| ;; | |
| *) | |
| # This is the profile name, break out of option parsing | |
| break | |
| ;; | |
| esac | |
| done | |
| # At this point, we should have a profile name | |
| if [[ $# -eq 0 ]]; then | |
| print_error "Profile name required" | |
| echo | |
| show_help | |
| exit 1 | |
| fi | |
| local profile_name=$1 | |
| shift | |
| # Validate profile name | |
| if [[ ! "$profile_name" =~ ^[a-zA-Z0-9_-]+$ ]]; then | |
| print_error "Invalid profile name. Use only letters, numbers, hyphens, and underscores" | |
| exit 1 | |
| fi | |
| # Launch VSCode with the profile | |
| launch_vscode "$profile_name" "$use_insiders" "$@" | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment