Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -65,6 +65,9 @@ process_host_flags() {
rebuild)
export REBUILD=true
;;
--global)
export GLOBAL_MODE=true
;;
tmux)
export CLAUDEBOX_WRAP_TMUX=true
;;
Expand Down
31 changes: 23 additions & 8 deletions lib/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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[@]}"
}

Expand Down
132 changes: 82 additions & 50 deletions lib/docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down
82 changes: 80 additions & 2 deletions lib/project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}"
}
Expand Down Expand Up @@ -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
Expand Down
29 changes: 23 additions & 6 deletions main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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

Expand Down