@@ -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
149181expand_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