Skip to content

Commit cb93e69

Browse files
MrFlounderclaude
andcommitted
feat: port spacing, MCP sync, and restart fixes
- Add port_spacing config for per-workspace port ranges (prevents collisions) - Add :port suffix in refs to extract just port number from URLs - Sync MCP servers from main repo to new workspaces - Fix port calculation to always use base from main repo (prevents compounding) - Fix restart_workspace to work with renamed tmux windows - Fix install_env to prepend to command instead of export Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5e40767 commit cb93e69

1 file changed

Lines changed: 154 additions & 47 deletions

File tree

src/crabcode

Lines changed: 154 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,38 @@ validate_config() {
145145
fi
146146
}
147147

148+
# Sync MCP servers from main repo to workspace in Claude Code settings
149+
sync_mcp_servers() {
150+
local workspace_dir=$1
151+
local main_repo=$(config_get "main_repo" "")
152+
153+
[ -z "$main_repo" ] && return
154+
155+
local claude_settings="$HOME/.claude/settings.json"
156+
[ ! -f "$claude_settings" ] && return
157+
158+
# Check if jq is available
159+
if ! command -v jq &>/dev/null; then
160+
return
161+
fi
162+
163+
# Get MCP servers from main repo
164+
local mcp_servers=$(jq -r --arg repo "$main_repo" '.projects[$repo].mcpServers // empty' "$claude_settings" 2>/dev/null)
165+
166+
# If main repo has no MCP servers, nothing to sync
167+
[ -z "$mcp_servers" ] || [ "$mcp_servers" = "null" ] || [ "$mcp_servers" = "{}" ] && return
168+
169+
# Update workspace's MCP servers
170+
local tmp_file=$(mktemp)
171+
if jq --arg ws "$workspace_dir" --argjson mcp "$mcp_servers" \
172+
'.projects[$ws].mcpServers = $mcp' "$claude_settings" > "$tmp_file" 2>/dev/null; then
173+
mv "$tmp_file" "$claude_settings"
174+
echo -e "${GREEN}Synced MCP servers from main repo${NC}"
175+
else
176+
rm -f "$tmp_file"
177+
fi
178+
}
179+
148180
# Expand ~ and environment variables in paths
149181
expand_path() {
150182
local path="$1"
@@ -230,8 +262,12 @@ get_workspace_ports() {
230262
local num=$1
231263
local dir=$2
232264

233-
local default_api=$((API_PORT_BASE + num))
234-
local default_app=$((APP_PORT_BASE + num))
265+
# Use port_spacing from config (default 10) for proper spacing between workspaces
266+
local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
267+
[ "$port_spacing" = "null" ] && port_spacing=10
268+
269+
local default_api=$((API_PORT_BASE + (num * port_spacing)))
270+
local default_app=$((APP_PORT_BASE + (num * port_spacing)))
235271

236272
# Read from .env if env_sync is configured
237273
local env_api=$(read_env_port "$dir" "api")
@@ -342,34 +378,45 @@ sync_env_files() {
342378
local port_var=$(echo "$ports_json" | yq -r ".[$p]" 2>/dev/null)
343379
[ -z "$port_var" ] || [ "$port_var" = "null" ] && continue
344380

345-
# Read current value from workspace .env
346-
local current_value=$(grep "^${port_var}=" "$full_path" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]"'"'")
381+
# ALWAYS read base value from main repo to get consistent base port
382+
# This prevents compounding port increments on repeated syncs
383+
local base_value=""
384+
local main_env="$MAIN_REPO/$env_path"
385+
[ -f "$main_env" ] && base_value=$(grep "^${port_var}=" "$main_env" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]"'"'")
347386

348-
# If no current value, try to read base from main repo
349-
if [ -z "$current_value" ]; then
350-
local main_env="$MAIN_REPO/$env_path"
351-
[ -f "$main_env" ] && current_value=$(grep "^${port_var}=" "$main_env" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]"'"'")
387+
# Fallback to workspace .env only if main repo doesn't have it
388+
if [ -z "$base_value" ]; then
389+
base_value=$(grep "^${port_var}=" "$full_path" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]"'"'")
352390
fi
353391

354-
# Extract base port number
392+
# Also read current workspace value for updating
393+
local current_value=$(grep "^${port_var}=" "$full_path" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]"'"'")
394+
[ -z "$current_value" ] && current_value="$base_value"
395+
396+
# Extract base port number from main repo value
355397
local base_port=""
356-
if echo "$current_value" | grep -qE '^[0-9]+$'; then
357-
base_port="$current_value"
358-
elif echo "$current_value" | grep -qE ':[0-9]+'; then
359-
base_port=$(echo "$current_value" | grep -oE ':[0-9]+' | tail -1 | tr -d ':')
398+
if echo "$base_value" | grep -qE '^[0-9]+$'; then
399+
base_port="$base_value"
400+
elif echo "$base_value" | grep -qE ':[0-9]+'; then
401+
base_port=$(echo "$base_value" | grep -oE ':[0-9]+' | tail -1 | tr -d ':')
360402
fi
361403

362404
[ -z "$base_port" ] && continue
363405

364-
# Calculate workspace port
365-
local new_port=$((base_port + workspace_num))
406+
# Get port spacing (default 10 to allow multiple services per workspace)
407+
local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
408+
[ "$port_spacing" = "null" ] && port_spacing=10
409+
410+
# Calculate workspace port: base + (workspace_num * spacing)
411+
# This ensures each workspace has a range of ports to avoid collisions
412+
local new_port=$((base_port + (workspace_num * port_spacing)))
366413

367414
# Check if port is available, find next free if not
368415
while lsof -i ":$new_port" &>/dev/null; do
369416
new_port=$((new_port + 1))
370-
# Safety limit
371-
if [ $new_port -gt $((base_port + 100)) ]; then
372-
new_port=$((base_port + workspace_num))
417+
# Safety limit - stay within this workspace's range
418+
if [ $new_port -gt $((base_port + (workspace_num * port_spacing) + port_spacing - 1)) ]; then
419+
new_port=$((base_port + (workspace_num * port_spacing)))
373420
break
374421
fi
375422
done
@@ -412,30 +459,52 @@ sync_env_files() {
412459
echo "$refs_keys" | while read -r ref_var; do
413460
[ -z "$ref_var" ] && continue
414461

415-
local ref_port_var=$(yq -r ".env_sync.files[$i].refs[\"$ref_var\"]" "$CONFIG_FILE" 2>/dev/null)
416-
[ -z "$ref_port_var" ] || [ "$ref_port_var" = "null" ] && continue
462+
local ref_source=$(yq -r ".env_sync.files[$i].refs[\"$ref_var\"]" "$CONFIG_FILE" 2>/dev/null)
463+
[ -z "$ref_source" ] || [ "$ref_source" = "null" ] && continue
464+
465+
# Check for :port suffix (e.g., API_URL:port extracts just the port number)
466+
local extract_port_only=false
467+
local ref_port_var="$ref_source"
468+
if [[ "$ref_source" == *":port" ]]; then
469+
extract_port_only=true
470+
ref_port_var="${ref_source%:port}"
471+
fi
417472

418473
# Look up the resolved port
419474
local resolved_port=$(grep "^${ref_port_var}=" "$ports_file" 2>/dev/null | cut -d= -f2)
420475
[ -z "$resolved_port" ] && continue
421476

422477
# Read current value
423478
local current_value=$(grep "^${ref_var}=" "$full_path" 2>/dev/null | cut -d= -f2-)
424-
[ -z "$current_value" ] && continue
425479

426-
# Extract current port from URL
427-
local current_port=$(echo "$current_value" | grep -oE ':[0-9]+' | tail -1 | tr -d ':')
428-
[ -z "$current_port" ] && continue
480+
if [ "$extract_port_only" = true ]; then
481+
# Set target to plain port number
482+
if [ "$current_value" != "$resolved_port" ]; then
483+
if [[ "$OSTYPE" == "darwin"* ]]; then
484+
sed -i '' "s|^${ref_var}=.*|${ref_var}=$resolved_port|" "$full_path"
485+
else
486+
sed -i "s|^${ref_var}=.*|${ref_var}=$resolved_port|" "$full_path"
487+
fi
488+
[ "$quiet" != "true" ] && echo -e " ${GREEN}$env_path: $ref_var$resolved_port${NC}"
489+
fi
490+
else
491+
# Replace port in URL (existing behavior)
492+
[ -z "$current_value" ] && continue
429493

430-
if [ "$current_port" != "$resolved_port" ]; then
431-
local new_value=$(echo "$current_value" | sed "s/:${current_port}/:${resolved_port}/g")
494+
# Extract current port from URL
495+
local current_port=$(echo "$current_value" | grep -oE ':[0-9]+' | tail -1 | tr -d ':')
496+
[ -z "$current_port" ] && continue
432497

433-
if [[ "$OSTYPE" == "darwin"* ]]; then
434-
sed -i '' "s|^${ref_var}=.*|${ref_var}=$new_value|" "$full_path"
435-
else
436-
sed -i "s|^${ref_var}=.*|${ref_var}=$new_value|" "$full_path"
498+
if [ "$current_port" != "$resolved_port" ]; then
499+
local new_value=$(echo "$current_value" | sed "s/:${current_port}/:${resolved_port}/g")
500+
501+
if [[ "$OSTYPE" == "darwin"* ]]; then
502+
sed -i '' "s|^${ref_var}=.*|${ref_var}=$new_value|" "$full_path"
503+
else
504+
sed -i "s|^${ref_var}=.*|${ref_var}=$new_value|" "$full_path"
505+
fi
506+
[ "$quiet" != "true" ] && echo -e " ${GREEN}$env_path: $ref_var → :$resolved_port${NC}"
437507
fi
438-
[ "$quiet" != "true" ] && echo -e " ${GREEN}$env_path: $ref_var → :$resolved_port${NC}"
439508
fi
440509
done
441510
done
@@ -457,8 +526,10 @@ show_ports() {
457526
continue
458527
fi
459528

460-
local default_api=$((API_PORT_BASE + i))
461-
local default_app=$((APP_PORT_BASE + i))
529+
local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
530+
[ "$port_spacing" = "null" ] && port_spacing=10
531+
local default_api=$((API_PORT_BASE + (i * port_spacing)))
532+
local default_app=$((APP_PORT_BASE + (i * port_spacing)))
462533
local api_port=$(read_env_port "$dir" "api")
463534
local app_port=$(read_env_port "$dir" "app")
464535
[ -z "$api_port" ] && api_port="$default_api"
@@ -577,12 +648,14 @@ create_workspace() {
577648
if [ -n "$install_cmd" ]; then
578649
echo -e "${YELLOW}Installing dependencies...${NC}"
579650
cd "$workspace_dir"
580-
# Export any env vars from install_env config
651+
# Get env vars from install_env config
581652
local install_env=$(config_get "install_env" "")
653+
local run_cmd="$install_cmd"
582654
if [ -n "$install_env" ] && [ "$install_env" != "null" ]; then
583-
export $install_env
655+
# Prepend env var to command so it's passed to subprocesses
656+
run_cmd="$install_env $install_cmd"
584657
fi
585-
if bash -c "$install_cmd"; then
658+
if bash -c "$run_cmd"; then
586659
touch "$workspace_dir/node_modules/.crabcode-installed"
587660
echo -e "${GREEN}Dependencies installed${NC}"
588661
else
@@ -600,14 +673,23 @@ create_workspace() {
600673
if [ -d "$workspace_dir/$sub_path" ]; then
601674
echo -e "${YELLOW}Installing $sub_path dependencies...${NC}"
602675
cd "$workspace_dir/$sub_path"
603-
if eval "$sub_install"; then
604-
touch "$workspace_dir/$sub_path/node_modules/.crabcode-installed"
676+
# Prepend env var to submodule install command too
677+
local sub_run_cmd="$sub_install"
678+
if [ -n "$install_env" ] && [ "$install_env" != "null" ]; then
679+
sub_run_cmd="$install_env $sub_install"
680+
fi
681+
if bash -c "$sub_run_cmd"; then
682+
# Only touch marker if node_modules exists
683+
[ -d "node_modules" ] && touch "node_modules/.crabcode-installed"
605684
fi
606685
cd "$workspace_dir"
607686
fi
608687
done
609688
fi
610689

690+
# Sync MCP servers from main repo to new workspace
691+
sync_mcp_servers "$workspace_dir"
692+
611693
success "Workspace $num created at $workspace_dir"
612694
}
613695

@@ -859,7 +941,9 @@ list_workspaces() {
859941
if [ -d "$dir" ]; then
860942
found=true
861943
local branch=$(cd "$dir" && git branch --show-current 2>/dev/null || echo "unknown")
862-
local api_port=$((API_PORT_BASE + i))
944+
local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
945+
[ "$port_spacing" = "null" ] && port_spacing=10
946+
local api_port=$((API_PORT_BASE + (i * port_spacing)))
863947
local env_port=$(read_env_port "$dir" "api")
864948
[ -n "$env_port" ] && api_port="$env_port"
865949

@@ -930,7 +1014,9 @@ interactive_workspace_menu() {
9301014
if [ -d "$dir" ]; then
9311015
found=true
9321016
local branch=$(cd "$dir" && git branch --show-current 2>/dev/null || echo "unknown")
933-
local api_port=$((API_PORT_BASE + i))
1017+
local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
1018+
[ "$port_spacing" = "null" ] && port_spacing=10
1019+
local api_port=$((API_PORT_BASE + (i * port_spacing)))
9341020
local env_port=$(read_env_port "$dir" "api")
9351021
[ -n "$env_port" ] && api_port="$env_port"
9361022

@@ -1153,12 +1239,14 @@ check_and_install_deps() {
11531239

11541240
if [ "$need_install" = "true" ]; then
11551241
echo -e "${YELLOW}Installing dependencies...${NC}"
1156-
# Export any env vars from install_env config
1242+
# Get env vars from install_env config
11571243
local install_env=$(config_get "install_env" "")
1244+
local run_cmd="$install_cmd"
11581245
if [ -n "$install_env" ] && [ "$install_env" != "null" ]; then
1159-
export $install_env
1246+
# Prepend env var to command so it's passed to subprocesses
1247+
run_cmd="$install_env $install_cmd"
11601248
fi
1161-
if bash -c "$install_cmd"; then
1249+
if bash -c "$run_cmd"; then
11621250
touch "$marker"
11631251
echo -e "${GREEN}Dependencies installed${NC}"
11641252
else
@@ -1190,8 +1278,14 @@ check_and_install_deps() {
11901278
if [ "$sub_need_install" = "true" ]; then
11911279
echo -e "${YELLOW}Installing $sub_path dependencies...${NC}"
11921280
cd "$sub_dir"
1193-
if eval "$sub_install"; then
1194-
touch "$sub_marker"
1281+
# Prepend env var to submodule install command too
1282+
local sub_run_cmd="$sub_install"
1283+
if [ -n "$install_env" ] && [ "$install_env" != "null" ]; then
1284+
sub_run_cmd="$install_env $sub_install"
1285+
fi
1286+
if bash -c "$sub_run_cmd"; then
1287+
# Only touch marker if node_modules exists
1288+
[ -d "node_modules" ] && touch "$sub_marker"
11951289
echo -e "${GREEN}$sub_path dependencies installed${NC}"
11961290
else
11971291
warn "Failed to install $sub_path dependencies"
@@ -1365,8 +1459,21 @@ restart_workspace() {
13651459

13661460
success "Git reset complete (on branch: $branch_name)"
13671461

1368-
# If we're in tmux and the window exists, restart the panes
1369-
if [ -n "$TMUX" ] && tmux list-windows -t "$SESSION_NAME" -F "#{window_name}" 2>/dev/null | grep -q "^$window_name$"; then
1462+
# If we're in tmux, restart the panes
1463+
# Use current window if we're in the workspace dir, otherwise look for ws$num
1464+
if [ -n "$TMUX" ]; then
1465+
local current_window=$(tmux display-message -p '#{window_name}' 2>/dev/null)
1466+
local current_dir=$(pwd)
1467+
local use_current_window=false
1468+
1469+
# If we're in the workspace directory, use current window (even if renamed)
1470+
if [[ "$current_dir" == "$dir"* ]]; then
1471+
use_current_window=true
1472+
window_name="$current_window"
1473+
fi
1474+
1475+
# Check if we can use current window or if ws$num exists
1476+
if [ "$use_current_window" = true ] || tmux list-windows -t "$SESSION_NAME" -F "#{window_name}" 2>/dev/null | grep -q "^$window_name$"; then
13701477
local port_info=$(get_workspace_ports "$num" "$dir")
13711478
local api_port=$(echo "$port_info" | cut -d: -f1)
13721479
local app_port=$(echo "$port_info" | cut -d: -f2)

0 commit comments

Comments
 (0)