diff --git a/lib/cli.sh b/lib/cli.sh index c70d574..8e8f636 100644 --- a/lib/cli.sh +++ b/lib/cli.sh @@ -7,7 +7,7 @@ # ============================================================================ # Four flag buckets (Bash 3.2 compatible - no associative arrays) -readonly HOST_ONLY_FLAGS=(--verbose rebuild) +readonly HOST_ONLY_FLAGS=(--verbose rebuild --global) readonly CONTROL_FLAGS=(--enable-sudo --disable-firewall) readonly SCRIPT_COMMANDS=(shell create slot slots revoke profiles projects profile info help -h --help add remove install allowlist clean save project tmux kill) @@ -65,6 +65,9 @@ process_host_flags() { rebuild) export REBUILD=true ;; + --global) + export GLOBAL_MODE=true + ;; tmux) export CLAUDEBOX_WRAP_TMUX=true ;; diff --git a/lib/config.sh b/lib/config.sh index 640211a..d42756d 100755 --- a/lib/config.sh +++ b/lib/config.sh @@ -97,11 +97,19 @@ expand_profile() { # -------- Profile file management --------------------------------------------- get_profile_file_path() { - # Use the parent directory name, not the slot name - local parent_name=$(generate_parent_folder_name "$PROJECT_DIR") - local parent_dir="$HOME/.claudebox/projects/$parent_name" - mkdir -p "$parent_dir" - echo "$parent_dir/profiles.ini" + # Check if global mode is enabled + if use_global_mode; then + # Use global profiles file + local global_dir="$HOME/.claudebox/global" + mkdir -p "$global_dir" + echo "$global_dir/profiles.ini" + else + # Use project-specific profiles file + local parent_name=$(generate_parent_folder_name "$PROJECT_DIR") + local parent_dir="$HOME/.claudebox/projects/$parent_name" + mkdir -p "$parent_dir" + echo "$parent_dir/profiles.ini" + fi } read_config_value() { @@ -177,15 +185,22 @@ update_profile_section() { } get_current_profiles() { - local profiles_file="${PROJECT_PARENT_DIR:-$HOME/.claudebox/projects/$(generate_parent_folder_name "$PWD")}/profiles.ini" + local profiles_file local current_profiles=() - + + # Check if global mode is enabled + if use_global_mode; then + profiles_file="$HOME/.claudebox/global/profiles.ini" + else + profiles_file="${PROJECT_PARENT_DIR:-$HOME/.claudebox/projects/$(generate_parent_folder_name "$PWD")}/profiles.ini" + fi + if [[ -f "$profiles_file" ]]; then while IFS= read -r line; do [[ -n "$line" ]] && current_profiles+=("$line") done < <(read_profile_section "$profiles_file" "profiles") fi - + printf '%s\n' "${current_profiles[@]}" } diff --git a/lib/docker.sh b/lib/docker.sh index 3e3fb50..19ed092 100755 --- a/lib/docker.sh +++ b/lib/docker.sh @@ -218,27 +218,46 @@ run_claudebox_container() { docker_args+=( -w /workspace -v "$PROJECT_DIR":/workspace - -v "$PROJECT_PARENT_DIR":/home/$DOCKER_USER/.claudebox ) - - # Ensure .claude directory exists - if [[ ! -d "$PROJECT_SLOT_DIR/.claude" ]]; then - mkdir -p "$PROJECT_SLOT_DIR/.claude" - fi - - docker_args+=(-v "$PROJECT_SLOT_DIR/.claude":/home/$DOCKER_USER/.claude) - - # Mount .claude.json only if it already exists (from previous session) - if [[ -f "$PROJECT_SLOT_DIR/.claude.json" ]]; then - docker_args+=(-v "$PROJECT_SLOT_DIR/.claude.json":/home/$DOCKER_USER/.claude.json) + + # Handle mounting based on global mode + if use_global_mode; then + # Global mode: Mount user's home directories directly + docker_args+=(-v "$HOME/.claudebox":/home/$DOCKER_USER/.claudebox) + docker_args+=(-v "$HOME/.claude":/home/$DOCKER_USER/.claude) + docker_args+=(-v "$HOME/.config":/home/$DOCKER_USER/.config) + docker_args+=(-v "$HOME/.cache":/home/$DOCKER_USER/.cache) + + if [[ "$VERBOSE" == "true" ]]; then + echo "[DEBUG] Global mode: mounting user's home directories directly" >&2 + fi + else + # Project mode: Mount project-specific slot directories + docker_args+=(-v "$PROJECT_PARENT_DIR":/home/$DOCKER_USER/.claudebox) + + # Ensure .claude directory exists + if [[ ! -d "$PROJECT_SLOT_DIR/.claude" ]]; then + mkdir -p "$PROJECT_SLOT_DIR/.claude" + fi + + docker_args+=(-v "$PROJECT_SLOT_DIR/.claude":/home/$DOCKER_USER/.claude) + + # Mount .claude.json only if it already exists (from previous session) + if [[ -f "$PROJECT_SLOT_DIR/.claude.json" ]]; then + docker_args+=(-v "$PROJECT_SLOT_DIR/.claude.json":/home/$DOCKER_USER/.claude.json) + fi + + # Mount .config directory + docker_args+=(-v "$PROJECT_SLOT_DIR/.config":/home/$DOCKER_USER/.config) + + # Mount .cache directory + docker_args+=(-v "$PROJECT_SLOT_DIR/.cache":/home/$DOCKER_USER/.cache) + + if [[ "$VERBOSE" == "true" ]]; then + echo "[DEBUG] Project mode: mounting project slot directories" >&2 + fi fi - # Mount .config directory - docker_args+=(-v "$PROJECT_SLOT_DIR/.config":/home/$DOCKER_USER/.config) - - # Mount .cache directory - docker_args+=(-v "$PROJECT_SLOT_DIR/.cache":/home/$DOCKER_USER/.cache) - # Mount SSH directory docker_args+=(-v "$HOME/.ssh":"/home/$DOCKER_USER/.ssh:ro") @@ -331,49 +350,62 @@ run_claudebox_container() { fi fi - # Create project MCP config file by merging project configs - # Start with empty config file for merging - local temp_project_file=$(mktemp /tmp/claudebox-project-temp-$(date +%s)-$$.json 2>/dev/null || mktemp) - mcp_temp_files+=("$temp_project_file") - echo '{"mcpServers":{}}' > "$temp_project_file" - - # Merge shared project settings first - local merged_file="" - if [[ -f "$PROJECT_DIR/.claude/settings.json" ]]; then - merged_file=$(create_mcp_config_file "$PROJECT_DIR/.claude/settings.json" "$temp_project_file") - if [[ -n "$merged_file" ]]; then - mv "$merged_file" "$temp_project_file" + # Handle project MCP configuration (skip in global mode) + if ! use_global_mode; then + # Create project MCP config file by merging project configs + # Start with empty config file for merging + local temp_project_file=$(mktemp /tmp/claudebox-project-temp-$(date +%s)-$$.json 2>/dev/null || mktemp) + mcp_temp_files+=("$temp_project_file") + echo '{"mcpServers":{}}' > "$temp_project_file" + + # Merge shared project settings first + local merged_file="" + if [[ -f "$PROJECT_DIR/.claude/settings.json" ]]; then + merged_file=$(create_mcp_config_file "$PROJECT_DIR/.claude/settings.json" "$temp_project_file") + if [[ -n "$merged_file" ]]; then + mv "$merged_file" "$temp_project_file" + fi fi - fi - - # Merge local project settings (highest priority) - if [[ -f "$PROJECT_DIR/.claude/settings.local.json" ]]; then - merged_file=$(create_mcp_config_file "$PROJECT_DIR/.claude/settings.local.json" "$temp_project_file") - if [[ -n "$merged_file" ]]; then - mv "$merged_file" "$temp_project_file" + + # Merge local project settings (highest priority) + if [[ -f "$PROJECT_DIR/.claude/settings.local.json" ]]; then + merged_file=$(create_mcp_config_file "$PROJECT_DIR/.claude/settings.local.json" "$temp_project_file") + if [[ -n "$merged_file" ]]; then + mv "$merged_file" "$temp_project_file" + fi fi - fi - - # Check if we have any project servers - local project_count=$(jq '.mcpServers | length' "$temp_project_file" 2>/dev/null || echo "0") - if [[ "$project_count" -gt 0 ]]; then - project_mcp_file="$temp_project_file" - if [[ "$VERBOSE" == "true" ]]; then - printf "Found %s project MCP servers\n" "$project_count" >&2 + + # Check if we have any project servers + local project_count=$(jq '.mcpServers | length' "$temp_project_file" 2>/dev/null || echo "0") + if [[ "$project_count" -gt 0 ]]; then + project_mcp_file="$temp_project_file" + if [[ "$VERBOSE" == "true" ]]; then + printf "Found %s project MCP servers\n" "$project_count" >&2 + fi + docker_args+=(-v "$project_mcp_file":/tmp/project-mcp-config.json:ro) + if [[ "$VERBOSE" == "true" ]]; then + echo "[DEBUG] Mounting project MCP configuration file" >&2 + fi + else + rm -f "$temp_project_file" + project_mcp_file="" fi - docker_args+=(-v "$project_mcp_file":/tmp/project-mcp-config.json:ro) + else if [[ "$VERBOSE" == "true" ]]; then - echo "[DEBUG] Mounting project MCP configuration file" >&2 + echo "[DEBUG] Global mode: skipping project MCP configuration" >&2 fi - else - rm -f "$temp_project_file" - project_mcp_file="" fi # Add environment variables local project_name=$(basename "$PROJECT_DIR") local slot_name=$(basename "$PROJECT_SLOT_DIR") + + # Adjust project_name and slot_name for global mode + if use_global_mode; then + project_name="global" + slot_name="global" + fi # Calculate slot index for hostname local slot_index=1 # default if we can't determine diff --git a/lib/project.sh b/lib/project.sh index 060ba05..414ec74 100755 --- a/lib/project.sh +++ b/lib/project.sh @@ -247,16 +247,87 @@ find_inactive_slot() { return 1 } +# ============================================================================ +# Global Mode Functions +# ============================================================================ + +# Check if global mode is enabled +use_global_mode() { + [[ "${GLOBAL_MODE:-false}" == "true" ]] +} + +# Initialize global mode directory structure +init_global_mode() { + local global_dir="$HOME/.claudebox/global" + + # Create global directory structure + mkdir -p "$global_dir" + + # Create symlinks to user's home directories + # Note: These symlinks are for bookkeeping; actual mounting happens in docker.sh + if [[ ! -L "$global_dir/.claude" ]]; then + ln -sf "$HOME/.claude" "$global_dir/.claude" + fi + + if [[ ! -L "$global_dir/.config" ]]; then + ln -sf "$HOME/.config" "$global_dir/.config" + fi + + if [[ ! -L "$global_dir/.cache" ]]; then + ln -sf "$HOME/.cache" "$global_dir/.cache" + fi + + # Create profiles.ini if it doesn't exist + [[ -f "$global_dir/profiles.ini" ]] || touch "$global_dir/profiles.ini" + + # Copy common.sh to global directory if it doesn't exist + local common_sh_target="$global_dir/common.sh" + if [[ ! -f "$common_sh_target" ]]; then + local common_sh_source="${CLAUDEBOX_SCRIPT_DIR:-${SCRIPT_DIR}}/lib/common.sh" + if [[ -f "$common_sh_source" ]]; then + cp "$common_sh_source" "$common_sh_target" + fi + fi +} + +# Get global mode directory +get_global_dir() { + echo "$HOME/.claudebox/global" +} + +# Get global mode container name (fixed name for global mode) +get_global_container_name() { + echo "global" +} + +# Check if we should use slot-based operations (false for global mode) +should_use_slots() { + if use_global_mode; then + return 1 # false - don't use slots + else + return 0 # true - use slots + fi +} + # ============================================================================ # Main Functions # ============================================================================ -# Get the project folder name - returns the next available slot +# Get the project folder name - returns the next available slot or global container get_project_folder_name() { local path="$1" + + # Check if global mode is enabled + if use_global_mode; then + # Initialize global mode and return fixed container name + init_global_mode + echo "global" + return 0 + fi + # First ensure project is initialized init_project_dir "$path" - + # Find next available slot local slot_name if slot_name=$(determine_next_start_container "$path"); then @@ -269,6 +340,12 @@ get_project_folder_name() { # Get Docker image name for a specific slot get_image_name() { + # For global mode, use a fixed image name + if use_global_mode; then + echo "claudebox-global" + return 0 + fi + local parent_folder_name=$(generate_parent_folder_name "${PROJECT_DIR}") printf 'claudebox-%s' "${parent_folder_name}" } @@ -614,6 +691,7 @@ export -f slugify_path generate_container_name generate_parent_folder_name get_p export -f init_project_dir init_slot_dir export -f read_counter write_counter export -f create_container determine_next_start_container find_ready_slot find_inactive_slot +export -f use_global_mode init_global_mode get_global_dir get_global_container_name should_use_slots export -f get_project_folder_name get_image_name _get_project_slug export -f get_project_by_path list_all_projects resolve_project_path export -f list_project_slots get_slot_dir get_slot_index prune_slot_counter diff --git a/main.sh b/main.sh index 0246f35..84e959c 100755 --- a/main.sh +++ b/main.sh @@ -256,10 +256,21 @@ main() { exit 0 fi - # Step 6: Initialize project directory (creates parent with profiles.ini) - init_project_dir "$PROJECT_DIR" - PROJECT_PARENT_DIR=$(get_parent_dir "$PROJECT_DIR") - export PROJECT_PARENT_DIR + # Step 6: Handle project vs global mode setup + if use_global_mode; then + # Global mode: set up global directory structure + if [[ "$VERBOSE" == "true" ]]; then + echo "[DEBUG] Global mode detected, initializing global configuration..." >&2 + fi + init_global_mode + PROJECT_PARENT_DIR=$(get_global_dir) + export PROJECT_PARENT_DIR + else + # Project mode: Initialize project directory (creates parent with profiles.ini) + init_project_dir "$PROJECT_DIR" + PROJECT_PARENT_DIR=$(get_parent_dir "$PROJECT_DIR") + export PROJECT_PARENT_DIR + fi # Step 7: Handle rebuild if requested (will use IMAGE_NAME from step 8) local rebuild_requested="${REBUILD:-false}" @@ -281,9 +292,15 @@ main() { IMAGE_NAME=$(get_image_name) export IMAGE_NAME - # Set PROJECT_SLOT_DIR if we have a slot + # Set PROJECT_SLOT_DIR if we have a slot (global mode uses "global" as slot name) if [[ -n "$project_folder_name" ]] && [[ "$project_folder_name" != "NONE" ]]; then - PROJECT_SLOT_DIR="$PROJECT_PARENT_DIR/$project_folder_name" + if use_global_mode && [[ "$project_folder_name" == "global" ]]; then + # In global mode, the slot directory is the global directory itself + PROJECT_SLOT_DIR="$PROJECT_PARENT_DIR" + else + # Project mode: use slot-specific directory + PROJECT_SLOT_DIR="$PROJECT_PARENT_DIR/$project_folder_name" + fi export PROJECT_SLOT_DIR fi