From 25f474b6c76290ed4f3d4fec70a8e8a82bfe5d32 Mon Sep 17 00:00:00 2001 From: Andrew Megli Date: Sat, 20 Sep 2025 22:16:05 -0400 Subject: [PATCH 1/8] Fix existing broken test --- tests/test_bash32_compat.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_bash32_compat.sh b/tests/test_bash32_compat.sh index 223e197..9b07a0f 100755 --- a/tests/test_bash32_compat.sh +++ b/tests/test_bash32_compat.sh @@ -77,7 +77,7 @@ test_get_all_names() { eval "$PROFILE_FUNCS" local result=$(get_all_profile_names) local count=$(echo "$result" | wc -w) - [[ $count -eq 20 ]] + [[ $count -eq 21 ]] } run_test "get_all_profile_names()" test_get_all_names @@ -186,4 +186,4 @@ else echo -e "${RED}Some tests failed ✗${NC}" echo "There may be compatibility issues" exit 1 -fi \ No newline at end of file +fi From 6fa5a3ee2d9558bff435c78aa2aaedb452b1d270 Mon Sep 17 00:00:00 2001 From: Andrew Megli Date: Sun, 21 Sep 2025 12:29:26 -0400 Subject: [PATCH 2/8] Apply defaults for various arrays to prevent errors when "set -u" is in play --- lib/cli.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/cli.sh b/lib/cli.sh index c70d574..5d3dbda 100644 --- a/lib/cli.sh +++ b/lib/cli.sh @@ -31,7 +31,7 @@ parse_cli_args() { # Single parsing loop - each arg goes into exactly ONE bucket local found_script_command=false - for arg in "${all_args[@]}"; do + for arg in "${all_args[@]:-}"; do if [[ " ${HOST_ONLY_FLAGS[*]} " == *" $arg "* ]]; then # Bucket 1: Host-only flags host_flags+=("$arg") @@ -49,15 +49,15 @@ parse_cli_args() { done # Export results for use by main script - export CLI_HOST_FLAGS=("${host_flags[@]}") - export CLI_CONTROL_FLAGS=("${control_flags[@]}") + export CLI_HOST_FLAGS=("${host_flags[@]:-}") + export CLI_CONTROL_FLAGS=("${control_flags[@]:-}") export CLI_SCRIPT_COMMAND="$script_command" - export CLI_PASS_THROUGH=("${pass_through[@]}") + export CLI_PASS_THROUGH=("${pass_through[@]:-}") } # Process host-only flags and set environment variables process_host_flags() { - for flag in "${CLI_HOST_FLAGS[@]}"; do + for flag in "${CLI_HOST_FLAGS[@]:-}"; do case "$flag" in --verbose) export VERBOSE=true @@ -134,4 +134,4 @@ debug_parsed_args() { } # Export all functions -export -f parse_cli_args process_host_flags get_command_requirements requires_docker_image requires_slot debug_parsed_args \ No newline at end of file +export -f parse_cli_args process_host_flags get_command_requirements requires_docker_image requires_slot debug_parsed_args From b5da853387c8a17bda898fc1861c881bdbd185e0 Mon Sep 17 00:00:00 2001 From: Andrew Megli Date: Mon, 22 Sep 2025 21:42:01 -0400 Subject: [PATCH 3/8] Extract test runner to separate file for future reuse --- tests/test_bash32_compat.sh | 68 ++++++------------------------------- tests/test_runner.sh | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 58 deletions(-) create mode 100644 tests/test_runner.sh diff --git a/tests/test_bash32_compat.sh b/tests/test_bash32_compat.sh index 9b07a0f..28de3ce 100755 --- a/tests/test_bash32_compat.sh +++ b/tests/test_bash32_compat.sh @@ -2,52 +2,21 @@ # Test script for Bash 3.2 compatibility # Run this with: bash test_bash32_compat.sh -echo "======================================" -echo "ClaudeBox Bash 3.2 Compatibility Test" -echo "======================================" -echo "Current Bash version: $BASH_VERSION" -echo +# Get test directory and source test runner +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$TEST_DIR/test_runner.sh" -# Colors (these should work in Bash 3.2) -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -# Test counter -TESTS_RUN=0 -TESTS_PASSED=0 - -# Test function -run_test() { - local test_name="$1" - local test_cmd="$2" - - TESTS_RUN=$((TESTS_RUN + 1)) - echo -n "Test $TESTS_RUN: $test_name... " - - if eval "$test_cmd" >/dev/null 2>&1; then - echo -e "${GREEN}PASS${NC}" - TESTS_PASSED=$((TESTS_PASSED + 1)) - return 0 - else - echo -e "${RED}FAIL${NC}" - echo " Error output:" - eval "$test_cmd" 2>&1 | sed 's/^/ /' - return 1 - fi -} +# Print header +print_test_header "ClaudeBox Bash 3.2 Compatibility Test" # Extract just the profile functions from config.sh -TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(dirname "$TEST_DIR")" CONFIG_SCRIPT="$ROOT_DIR/lib/config.sh" # Extract the profile functions - they start at get_profile_packages and end at profile_exists # Include the entire profile_exists function by searching for the next function after it PROFILE_FUNCS=$(sed -n '/^get_profile_packages()/,/^expand_profile()/p' "$CONFIG_SCRIPT" | sed '$d') -echo "1. Testing profile functions" -echo "----------------------------" +print_section "1. Testing profile functions" # Test 1: Basic function sourcing test_basic_sourcing() { @@ -131,9 +100,7 @@ test_invalid_profile() { } run_test "Invalid profile handling" test_invalid_profile -echo -echo "3. Testing Bash 3.2 specific issues" -echo "-----------------------------------" +print_section "3. Testing Bash 3.2 specific issues" # Test 10: No associative arrays test_no_associative_arrays() { @@ -169,21 +136,6 @@ test_with_set_u() { } run_test "Functions work with set -u" test_with_set_u -echo -echo "======================================" -echo "Test Summary" -echo "======================================" -echo "Tests run: $TESTS_RUN" -echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}" -echo -e "Tests failed: ${RED}$((TESTS_RUN - TESTS_PASSED))${NC}" -echo - -if [[ $TESTS_PASSED -eq $TESTS_RUN ]]; then - echo -e "${GREEN}All tests passed! ✓${NC}" - echo "The script should work with Bash 3.2" - exit 0 -else - echo -e "${RED}Some tests failed ✗${NC}" - echo "There may be compatibility issues" - exit 1 -fi +# Print summary and exit with appropriate code +print_test_summary +exit $? diff --git a/tests/test_runner.sh b/tests/test_runner.sh new file mode 100644 index 0000000..18d18bf --- /dev/null +++ b/tests/test_runner.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Reusable test runner for ClaudeBox tests +# Source this file to use the test framework + +# Colors (Bash 3.2 compatible) +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +TESTS_RUN=0 +TESTS_PASSED=0 + +run_test() { + local test_name="$1" + local test_cmd="$2" + + TESTS_RUN=$((TESTS_RUN + 1)) + echo -n "Test $TESTS_RUN: $test_name... " + + if eval "$test_cmd" >/dev/null 2>&1; then + echo -e "${GREEN}PASS${NC}" + TESTS_PASSED=$((TESTS_PASSED + 1)) + return 0 + else + echo -e "${RED}FAIL${NC}" + echo " Error output:" + eval "$test_cmd" 2>&1 | sed 's/^/ /' + return 1 + fi +} + +print_test_summary() { + echo + echo "======================================" + echo "Test Summary" + echo "======================================" + echo "Tests run: $TESTS_RUN" + echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}" + echo -e "Tests failed: ${RED}$((TESTS_RUN - TESTS_PASSED))${NC}" + echo + + if [[ $TESTS_PASSED -eq $TESTS_RUN ]]; then + echo -e "${GREEN}All tests passed! ✓${NC}" + return 0 + else + echo -e "${RED}Some tests failed ✗${NC}" + return 1 + fi +} + +print_test_header() { + local title="$1" + echo "======================================" + echo "$title" + echo "======================================" + echo "Current Bash version: $BASH_VERSION" + echo +} + +print_section() { + local section="$1" + echo + echo "$section" + echo "$(echo "$section" | sed 's/./-/g')" +} From fba9d2e1eb4c9c7e5ac1395b9b72441413139b6f Mon Sep 17 00:00:00 2001 From: Andrew Megli Date: Mon, 22 Sep 2025 21:46:01 -0400 Subject: [PATCH 4/8] Add installation test to recreate MacOS (bash 3.2) errors --- tests/docker_test_build.sh | 36 ++++++++++++++++++++++++++++ tests/test_bash32_compat.sh | 7 ++---- tests/test_in_bash32_docker.sh | 18 ++++++++++---- tests/test_install.sh | 44 ++++++++++++++++++++++++++++++++++ tests/test_runner.sh | 1 - 5 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 tests/docker_test_build.sh create mode 100755 tests/test_install.sh diff --git a/tests/docker_test_build.sh b/tests/docker_test_build.sh new file mode 100644 index 0000000..2d2036f --- /dev/null +++ b/tests/docker_test_build.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Docker-based ClaudeBox build and install test +# This script runs inside the Docker container + +set -euo pipefail + +bash .builder/build.sh || { + echo "FAIL - Build failed with exit code: $?" >&2 + exit 1 +} + +if [[ ! -f dist/claudebox.run ]]; then + echo "FAIL - Build output not found." >&2 + exit 1 +fi + +if [[ ! -x dist/claudebox.run ]]; then + echo "FAIL - Build output is not executable." >&2 + exit 1 +fi + +bash ./dist/claudebox.run || { + echo "FAIL - Installation failed with exit code: $?" >&2 + exit 1 +} + +if [[ ! -d "$HOME/.claudebox" ]]; then + echo "FAIL - Installation directory not found." >&2 + exit 1 +fi + +export PATH="$HOME/.local/bin:$PATH" +claudebox --help >/dev/null || { + echo "claudebox command failed!" >&2 + exit 1 +} diff --git a/tests/test_bash32_compat.sh b/tests/test_bash32_compat.sh index 28de3ce..1298d31 100755 --- a/tests/test_bash32_compat.sh +++ b/tests/test_bash32_compat.sh @@ -2,12 +2,11 @@ # Test script for Bash 3.2 compatibility # Run this with: bash test_bash32_compat.sh -# Get test directory and source test runner TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$TEST_DIR/test_runner.sh" # Print header -print_test_header "ClaudeBox Bash 3.2 Compatibility Test" +print_test_header "ClaudeBox Bash Compatibility Test" # Extract just the profile functions from config.sh ROOT_DIR="$(dirname "$TEST_DIR")" @@ -57,9 +56,7 @@ test_profile_exists() { } run_test "profile_exists()" test_profile_exists -echo -echo "2. Testing usage patterns from main script" -echo "------------------------------------------" +print_section "2. Testing usage patterns from main script" # Test 6: Pattern used in profiles command test_profiles_pattern() { diff --git a/tests/test_in_bash32_docker.sh b/tests/test_in_bash32_docker.sh index 5dd3515..f367cf3 100755 --- a/tests/test_in_bash32_docker.sh +++ b/tests/test_in_bash32_docker.sh @@ -28,16 +28,26 @@ docker run --rm \ bash:3.2 \ bash /workspace/tests/test_bash32_compat.sh +# Clean up the Bash 3.2 image +echo +echo "Cleaning up bash:3.2 image..." +docker rmi bash:3.2 >/dev/null 2>&1 || true +echo "Test complete." + # Also test with Bash 4+ for comparison echo echo "==========================================" echo "Running same tests in Bash 4+ for comparison..." echo "==========================================" echo -bash "$SCRIPT_DIR/test_bash32_compat.sh" +echo "Running tests in Bash 4.0..." +echo +docker run --rm \ + -v "$PARENT_DIR":/workspace \ + bash:4.0 \ + bash /workspace/tests/test_bash32_compat.sh -# Clean up the Bash 3.2 image echo -echo "Cleaning up bash:3.2 image..." -docker rmi bash:3.2 >/dev/null 2>&1 || true +echo "Cleaning up bash:4.0 image..." +docker rmi bash:4.0 >/dev/null 2>&1 || true echo "Test complete." diff --git a/tests/test_install.sh b/tests/test_install.sh new file mode 100755 index 0000000..ff0e62b --- /dev/null +++ b/tests/test_install.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Run ClaudeBox installation tests using Docker +# Usage: ./test_install.sh + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$TEST_DIR/test_runner.sh" +PARENT_DIR="$(dirname "$TEST_DIR")" + +print_test_header "ClaudeBox Docker Build/Install - 3.2" +test_docker_build_install_bash32() { + docker run --rm \ + -v "$PARENT_DIR":/workspace \ + -w /workspace \ + bash:3.2 \ + bash tests/docker_test_build.sh +} +run_test "Build and install in Bash 3.2" test_docker_build_install_bash32 + +print_test_header "ClaudeBox Docker Build/Install - 4.0" +test_docker_build_install_bash4() { + docker run --rm \ + -v "$PARENT_DIR":/workspace \ + -w /workspace \ + bash:4.0 \ + bash tests/docker_test_build.sh +} +run_test "Build and install in Bash 4.0" test_docker_build_install_bash4 + +print_test_header "ClaudeBox Docker Build/Install - 5.0" +test_docker_build_install_bash5() { + docker run --rm \ + -v "$PARENT_DIR":/workspace \ + -w /workspace \ + bash:5.0 \ + bash tests/docker_test_build.sh +} +run_test "Build and install in Bash 5.0" test_docker_build_install_bash5 + +docker rmi bash:3.2 >/dev/null 2>&1 || true +docker rmi bash:4.0 >/dev/null 2>&1 || true +docker rmi bash:5.0 >/dev/null 2>&1 || true + +print_test_summary +exit $? diff --git a/tests/test_runner.sh b/tests/test_runner.sh index 18d18bf..8528d47 100644 --- a/tests/test_runner.sh +++ b/tests/test_runner.sh @@ -54,7 +54,6 @@ print_test_header() { echo "======================================" echo "$title" echo "======================================" - echo "Current Bash version: $BASH_VERSION" echo } From c98212912cf574aca2035629e090f423e04af562 Mon Sep 17 00:00:00 2001 From: Andrew Megli Date: Tue, 23 Sep 2025 09:57:15 -0400 Subject: [PATCH 5/8] Expand docker install tests --- tests/docker_test_build.sh | 16 +++++++++++++++- tests/test_install.sh | 10 +++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/docker_test_build.sh b/tests/docker_test_build.sh index 2d2036f..e1574aa 100644 --- a/tests/docker_test_build.sh +++ b/tests/docker_test_build.sh @@ -31,6 +31,20 @@ fi export PATH="$HOME/.local/bin:$PATH" claudebox --help >/dev/null || { - echo "claudebox command failed!" >&2 + echo "claudebox command failed." >&2 exit 1 } +claudebox create >/dev/null || { + echo "claudebox create command failed." >&2 + exit 1 +} +claudebox slots >/dev/null || { + echo "claudebox slots command failed." >&2 + exit 1 +} +claudebox projects >/dev/null || { + echo "claudebox projects command failed." >&2 + exit 1 +} + +# TODO - use dind to test commands that require docker diff --git a/tests/test_install.sh b/tests/test_install.sh index ff0e62b..0d212a6 100755 --- a/tests/test_install.sh +++ b/tests/test_install.sh @@ -6,7 +6,7 @@ TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$TEST_DIR/test_runner.sh" PARENT_DIR="$(dirname "$TEST_DIR")" -print_test_header "ClaudeBox Docker Build/Install - 3.2" +print_test_header "ClaudeBox Docker Build/Install - Bash 3.2" test_docker_build_install_bash32() { docker run --rm \ -v "$PARENT_DIR":/workspace \ @@ -16,7 +16,7 @@ test_docker_build_install_bash32() { } run_test "Build and install in Bash 3.2" test_docker_build_install_bash32 -print_test_header "ClaudeBox Docker Build/Install - 4.0" +print_test_header "ClaudeBox Docker Build/Install - Bash 4.0" test_docker_build_install_bash4() { docker run --rm \ -v "$PARENT_DIR":/workspace \ @@ -26,19 +26,19 @@ test_docker_build_install_bash4() { } run_test "Build and install in Bash 4.0" test_docker_build_install_bash4 -print_test_header "ClaudeBox Docker Build/Install - 5.0" +print_test_header "ClaudeBox Docker Build/Install - Bash 5.0" test_docker_build_install_bash5() { docker run --rm \ -v "$PARENT_DIR":/workspace \ -w /workspace \ - bash:5.0 \ + bash:5.3 \ bash tests/docker_test_build.sh } run_test "Build and install in Bash 5.0" test_docker_build_install_bash5 docker rmi bash:3.2 >/dev/null 2>&1 || true docker rmi bash:4.0 >/dev/null 2>&1 || true -docker rmi bash:5.0 >/dev/null 2>&1 || true +docker rmi bash:5.3 >/dev/null 2>&1 || true print_test_summary exit $? From b9ff9bb45b1f4d1c1e5f66b3dd8c9a5df9683a5f Mon Sep 17 00:00:00 2001 From: Andrew Megli Date: Tue, 23 Sep 2025 09:57:34 -0400 Subject: [PATCH 6/8] Add expansion defaults for a number of files to prevent post-install issues with MacOS --- lib/cli.sh | 10 +++++----- lib/commands.clean.sh | 8 ++++---- lib/commands.core.sh | 6 +++--- lib/commands.profile.sh | 18 +++++++++--------- lib/commands.system.sh | 24 ++++++++++++------------ lib/config.sh | 14 +++++++------- lib/docker.sh | 6 +++--- main.sh | 20 ++++++++++---------- 8 files changed, 53 insertions(+), 53 deletions(-) diff --git a/lib/cli.sh b/lib/cli.sh index 5d3dbda..1b928b3 100644 --- a/lib/cli.sh +++ b/lib/cli.sh @@ -31,7 +31,7 @@ parse_cli_args() { # Single parsing loop - each arg goes into exactly ONE bucket local found_script_command=false - for arg in "${all_args[@]:-}"; do + for arg in "${all_args[@]:+${all_args[@]}}"; do if [[ " ${HOST_ONLY_FLAGS[*]} " == *" $arg "* ]]; then # Bucket 1: Host-only flags host_flags+=("$arg") @@ -49,15 +49,15 @@ parse_cli_args() { done # Export results for use by main script - export CLI_HOST_FLAGS=("${host_flags[@]:-}") - export CLI_CONTROL_FLAGS=("${control_flags[@]:-}") + export CLI_HOST_FLAGS=("${host_flags[@]:+${host_flags[@]}}") + export CLI_CONTROL_FLAGS=("${control_flags[@]:+${control_flags[@]}}") export CLI_SCRIPT_COMMAND="$script_command" - export CLI_PASS_THROUGH=("${pass_through[@]:-}") + export CLI_PASS_THROUGH=("${pass_through[@]:+${pass_through[@]}}") } # Process host-only flags and set environment variables process_host_flags() { - for flag in "${CLI_HOST_FLAGS[@]:-}"; do + for flag in "${CLI_HOST_FLAGS[@]:+${CLI_HOST_FLAGS[@]}}"; do case "$flag" in --verbose) export VERBOSE=true diff --git a/lib/commands.clean.sh b/lib/commands.clean.sh index 775c921..9615c04 100644 --- a/lib/commands.clean.sh +++ b/lib/commands.clean.sh @@ -76,9 +76,9 @@ _cmd_clean() { done # Handle results - if [ ${#matches[@]} -eq 0 ]; then + if [[ ${#matches[@]} -eq 0 ]]; then error "No project found matching: $search" - elif [ ${#matches[@]} -eq 1 ]; then + elif [[ ${#matches[@]} -eq 1 ]]; then # Single match - clean it local project_info="${matches[0]}" local project_path="${project_info%|*}" @@ -101,7 +101,7 @@ _cmd_clean() { if [[ "$choice" == "q" ]] || [[ -z "$choice" ]]; then exit 0 - elif [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#matches[@]}" ]; then + elif [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le "${#matches[@]}" ]]; then local selected="${matches[$((choice-1))]}" local project_path="${selected%|*}" local project_name="${selected#*|}" @@ -258,4 +258,4 @@ _cmd_redo() { } # Export functions -export -f _cmd_clean _clean_project _cmd_undo _cmd_redo \ No newline at end of file +export -f _cmd_clean _clean_project _cmd_undo _cmd_redo diff --git a/lib/commands.core.sh b/lib/commands.core.sh index 08d97bc..4cc9ab3 100644 --- a/lib/commands.core.sh +++ b/lib/commands.core.sh @@ -117,7 +117,7 @@ _cmd_shell() { echo "[DEBUG] Remaining args after processing: $*" >&2 fi # Don't pass any remaining arguments - only shell and the flags - run_claudebox_container "$temp_container" "interactive" shell "${shell_flags[@]}" + run_claudebox_container "$temp_container" "interactive" shell "${shell_flags[@]:+${shell_flags[@]}}" # Commit changes back to image fillbar @@ -127,7 +127,7 @@ _cmd_shell() { success "Changes saved to image!" else # Regular shell mode - just run without committing - run_claudebox_container "" "interactive" shell "${shell_flags[@]}" + run_claudebox_container "" "interactive" shell "${shell_flags[@]:+${shell_flags[@]}}" fi exit 0 @@ -230,4 +230,4 @@ _cmd_update() { _cmd_special "update" "$@" } -export -f _cmd_help _cmd_shell _cmd_update \ No newline at end of file +export -f _cmd_help _cmd_shell _cmd_update diff --git a/lib/commands.profile.sh b/lib/commands.profile.sh index 462d5d4..7342232 100644 --- a/lib/commands.profile.sh +++ b/lib/commands.profile.sh @@ -32,7 +32,7 @@ _cmd_profiles() { local desc=$(get_profile_description "$profile") local is_enabled=false # Check if profile is currently enabled - for enabled in "${current_profiles[@]}"; do + for enabled in "${current_profiles[@]:-}"; do if [[ "$enabled" == "$profile" ]]; then is_enabled=true break @@ -127,7 +127,7 @@ _cmd_add() { [[ ${#selected[@]} -eq 0 ]] && error "No valid profiles specified\nRun 'claudebox profiles' to see available profiles" - update_profile_section "$profile_file" "profiles" "${selected[@]}" + update_profile_section "$profile_file" "profiles" "${selected[@]:+${selected[@]}}" local all_profiles=() local all_profiles=() @@ -144,7 +144,7 @@ _cmd_add() { # Check if any Python-related profiles were added local python_profiles_added=false - for profile in "${selected[@]}"; do + for profile in "${selected[@]:+${selected[@]}}"; do if [[ "$profile" == "python" ]] || [[ "$profile" == "ml" ]] || [[ "$profile" == "datascience" ]]; then python_profiles_added=true break @@ -162,7 +162,7 @@ _cmd_add() { # Only show rebuild message for non-Python profiles local needs_rebuild=false - for profile in "${selected[@]}"; do + for profile in "${selected[@]:+${selected[@]}}"; do if [[ "$profile" != "python" ]] && [[ "$profile" != "ml" ]] && [[ "$profile" != "datascience" ]]; then needs_rebuild=true break @@ -229,9 +229,9 @@ _cmd_remove() { # Remove specified profiles local new_profiles=() local python_profiles_removed=false - for profile in "${current_profiles[@]}"; do + for profile in "${current_profiles[@]:+${current_profiles[@]}}"; do local keep=true - for remove in "${to_remove[@]}"; do + for remove in "${to_remove[@]:+${to_remove[@]}}"; do if [[ "$profile" == "$remove" ]]; then keep=false # Check if we're removing a Python-related profile @@ -246,7 +246,7 @@ _cmd_remove() { # Check if any Python-related profiles remain local has_python_profiles=false - for profile in "${new_profiles[@]}"; do + for profile in "${new_profiles[@]:+${new_profiles[@]}}"; do if [[ "$profile" == "python" ]] || [[ "$profile" == "ml" ]] || [[ "$profile" == "datascience" ]]; then has_python_profiles=true break @@ -275,7 +275,7 @@ _cmd_remove() { # Write back the filtered profiles { echo "[profiles]" - for profile in "${new_profiles[@]}"; do + for profile in "${new_profiles[@]:+${new_profiles[@]}}"; do echo "$profile" done echo "" @@ -323,4 +323,4 @@ _cmd_install() { echo } -export -f _cmd_profiles _cmd_profile _cmd_add _cmd_remove _cmd_install \ No newline at end of file +export -f _cmd_profiles _cmd_profile _cmd_add _cmd_remove _cmd_install diff --git a/lib/commands.system.sh b/lib/commands.system.sh index 75f7f7f..91dd3c8 100644 --- a/lib/commands.system.sh +++ b/lib/commands.system.sh @@ -421,7 +421,7 @@ Current directory: $PWD" if [[ ${#window_panes[@]} -gt 0 ]]; then # Calculate total slots needed total_slots_needed=0 - for panes in "${window_panes[@]}"; do + for panes in "${window_panes[@]:+${window_panes[@]}}"; do ((total_slots_needed += panes)) || true done @@ -519,10 +519,10 @@ Current directory: $PWD" # Send activate command only to authenticated slots if [[ "$VERBOSE" == "true" ]]; then - echo "[DEBUG] Checking authentication for ${#captured_panes[@]} panes" >&2 + echo "[DEBUG] Checking authentication for ${#captured_panes[@]:-} panes" >&2 fi - for ((i=0; i<${#captured_panes[@]}; i++)); do + for ((i=0; i<${#captured_panes[@]:-0}; i++)); do local pane_id="${captured_panes[$i]}" local slot_num="${available_slots[$i]}" local slot_name=$(generate_container_name "$PROJECT_DIR" "$slot_num") @@ -558,7 +558,7 @@ Current directory: $PWD" local first_created=false # Create panes one by one, capturing IDs atomically - for panes in "${window_panes[@]}"; do + for panes in "${window_panes[@]:+${window_panes[@]}}"; do for ((i=0; i&2 + echo "[DEBUG] Checking authentication for ${#captured_panes[@]:-} panes" >&2 fi - for ((i=0; i<${#captured_panes[@]}; i++)); do + for ((i=0; i<${#captured_panes[@]:-0}; i++)); do local pane_id="${captured_panes[$i]}" local slot_num="${available_slots[$i]}" local slot_name=$(generate_container_name "$PROJECT_DIR" "$slot_num") @@ -702,9 +702,9 @@ _cmd_project() { done # Handle results - if [ ${#matches[@]} -eq 0 ]; then + if [[ ${#matches[@]} -eq 0 ]]; then error "No projects found matching '$search'" - elif [ ${#matches[@]} -eq 1 ]; then + elif [[ ${#matches[@]} -eq 1 ]]; then # Single match - use it local project_path="${matches[0]%%|*}" local project_name="${matches[0]##*|}" @@ -726,7 +726,7 @@ _cmd_project() { else # Multiple matches - show them error "Multiple projects match '$search':" - for match in "${matches[@]}"; do + for match in "${matches[@]:-}"; do local path="${match%%|*}" local name="${match##*|}" echo " $name -> $path" @@ -813,7 +813,7 @@ _cmd_import() { cecho "Available commands to import:" "$CYAN" echo local i=1 - for cmd in "${commands[@]}"; do + for cmd in "${commands[@]:-}"; do printf " %2d. %s\n" "$i" "$cmd" ((i++)) || true done @@ -832,7 +832,7 @@ _cmd_import() { a|A|all|ALL) # Import all commands local imported=0 - for cmd in "${commands[@]}"; do + for cmd in "${commands[@]:+${commands[@]}}"; do if cp "$host_commands/$cmd" "$project_commands/"; then ((imported++)) || true fi @@ -938,4 +938,4 @@ _install_tmux_conf() { fi } -export -f _cmd_save _cmd_unlink _cmd_rebuild _cmd_tmux _cmd_project _cmd_special _cmd_import _install_tmux_conf _cmd_kill \ No newline at end of file +export -f _cmd_save _cmd_unlink _cmd_rebuild _cmd_tmux _cmd_project _cmd_special _cmd_import _install_tmux_conf _cmd_kill diff --git a/lib/config.sh b/lib/config.sh index 640211a..eb2669c 100755 --- a/lib/config.sh +++ b/lib/config.sh @@ -130,7 +130,7 @@ read_profile_section() { done < <(sed -n "/^\[$section\]/,/^\[/p" "$profile_file" | tail -n +2 | grep -v '^\[') fi - printf '%s\n' "${result[@]}" + printf '%s\n' "${result[@]:-}" } update_profile_section() { @@ -143,13 +143,13 @@ update_profile_section() { readarray -t existing_items < <(read_profile_section "$profile_file" "$section") local all_items=() - for item in "${existing_items[@]}"; do + for item in "${existing_items[@]:+${existing_items[@]}}"; do [[ -n "$item" ]] && all_items+=("$item") done - for item in "${new_items[@]}"; do + for item in "${new_items[@]:+${new_items[@]}}"; do local found=false - for existing in "${all_items[@]}"; do + for existing in "${all_items[@]:+${all_items[@]}}"; do [[ "$existing" == "$item" ]] && found=true && break done [[ "$found" == "false" ]] && all_items+=("$item") @@ -169,7 +169,7 @@ update_profile_section() { fi echo "[$section]" - for item in "${all_items[@]}"; do + for item in "${all_items[@]:+${all_items[@]}}"; do echo "$item" done echo "" @@ -186,7 +186,7 @@ get_current_profiles() { done < <(read_profile_section "$profiles_file" "profiles") fi - printf '%s\n' "${current_profiles[@]}" + printf '%s\n' "${current_profiles[@]:-}" } # -------- Profile installation functions for Docker builds ------------------- @@ -370,4 +370,4 @@ export -f get_profile_file_path read_config_value read_profile_section update_pr export -f get_profile_core get_profile_build_tools get_profile_shell get_profile_networking get_profile_c get_profile_openwrt export -f get_profile_rust get_profile_python get_profile_go get_profile_flutter get_profile_javascript get_profile_java get_profile_ruby export -f get_profile_php get_profile_database get_profile_devops get_profile_web get_profile_embedded get_profile_datascience -export -f get_profile_security get_profile_ml \ No newline at end of file +export -f get_profile_security get_profile_ml diff --git a/lib/docker.sh b/lib/docker.sh index 3e3fb50..2efd8da 100755 --- a/lib/docker.sh +++ b/lib/docker.sh @@ -94,7 +94,7 @@ run_claudebox_container() { # Handle "attached" mode - start detached, wait, then attach if [[ "$run_mode" == "attached" ]]; then # Start detached - run_claudebox_container "$container_name" "detached" "${container_args[@]}" >/dev/null + run_claudebox_container "$container_name" "detached" "${container_args[@]:+${container_args[@]}}" >/dev/null # Show progress while container initializes fillbar @@ -296,7 +296,7 @@ run_claudebox_container() { # Set up cleanup trap for temporary MCP config files cleanup_mcp_files() { local file - for file in "${mcp_temp_files[@]}"; do + for file in "${mcp_temp_files[@]:+${mcp_temp_files[@]}}"; do if [[ -f "$file" ]]; then rm -f "$file" fi @@ -451,4 +451,4 @@ run_docker_build() { -f "$1" -t "$IMAGE_NAME" "$2" || error "Docker build failed" } -export -f check_docker install_docker configure_docker_nonroot docker_exec_root docker_exec_user run_claudebox_container check_container_exists run_docker_build \ No newline at end of file +export -f check_docker install_docker configure_docker_nonroot docker_exec_root docker_exec_user run_claudebox_container check_container_exists run_docker_build diff --git a/main.sh b/main.sh index 0246f35..5194a87 100755 --- a/main.sh +++ b/main.sh @@ -137,7 +137,7 @@ main() { # If command doesn't need Docker, skip all Docker setup if [[ "$cmd_requirements" == "none" ]]; then # Dispatch the command directly and exit - dispatch_command "${CLI_SCRIPT_COMMAND}" "${CLI_PASS_THROUGH[@]}" "${CLI_CONTROL_FLAGS[@]}" + dispatch_command "${CLI_SCRIPT_COMMAND}" "${CLI_PASS_THROUGH[@]:+${CLI_PASS_THROUGH[@]}}" "${CLI_CONTROL_FLAGS[@]:+${CLI_CONTROL_FLAGS[@]}}" exit $? fi @@ -299,7 +299,7 @@ main() { local cmd_req=$(get_command_requirements "${CLI_SCRIPT_COMMAND}") # Only run pre-flight for commands that need Docker or image if [[ "$cmd_req" == "docker" ]] || [[ "$cmd_req" == "image" ]]; then - if ! preflight_check "${CLI_SCRIPT_COMMAND}" "${CLI_PASS_THROUGH[@]}"; then + if ! preflight_check "${CLI_SCRIPT_COMMAND}" "${CLI_PASS_THROUGH[@]:+${CLI_PASS_THROUGH[@]}}"; then # Pre-flight check failed and printed error exit 1 fi @@ -353,7 +353,7 @@ main() { local docker_profiles=() local python_only_profiles=("python" "ml" "datascience") - for profile in "${current_profiles[@]}"; do + for profile in "${current_profiles[@]:+${current_profiles[@]}}"; do local is_python_only=false for py_profile in "${python_only_profiles[@]}"; do if [[ "$profile" == "$py_profile" ]]; then @@ -426,7 +426,7 @@ main() { if [[ -n "${CLI_SCRIPT_COMMAND}" ]]; then # Script command - dispatch on host # Pass control flags and pass-through args to dispatch_command - dispatch_command "${CLI_SCRIPT_COMMAND}" "${CLI_PASS_THROUGH[@]}" "${CLI_CONTROL_FLAGS[@]}" + dispatch_command "${CLI_SCRIPT_COMMAND}" "${CLI_PASS_THROUGH[@]:-}" "${CLI_CONTROL_FLAGS[@]:-}" exit $? else # No script command - running Claude interactively @@ -472,7 +472,7 @@ main() { # Check if stdin is not a terminal (i.e., we're receiving piped input) # and -p/--print flag isn't already present local has_print_flag=false - for arg in "${CLI_PASS_THROUGH[@]}"; do + for arg in "${CLI_PASS_THROUGH[@]:-}"; do if [[ "$arg" == "-p" ]] || [[ "$arg" == "--print" ]]; then has_print_flag=true break @@ -495,9 +495,9 @@ main() { fi local piped_input piped_input=$(cat) - run_claudebox_container "$container_name" "interactive" "${CLI_CONTROL_FLAGS[@]}" "-p" "$piped_input" "${CLI_PASS_THROUGH[@]}" + run_claudebox_container "$container_name" "interactive" "${CLI_CONTROL_FLAGS[@]:-}" "-p" "$piped_input" "${CLI_PASS_THROUGH[@]:-}" else - run_claudebox_container "$container_name" "interactive" "${CLI_CONTROL_FLAGS[@]}" "${CLI_PASS_THROUGH[@]}" + run_claudebox_container "$container_name" "interactive" "${CLI_CONTROL_FLAGS[@]:-}" "${CLI_PASS_THROUGH[@]:-}" fi else show_no_slots_menu @@ -537,7 +537,7 @@ build_docker_image() { done < <(read_profile_section "$profiles_file" "profiles") # Generate profile installations - for profile in "${current_profiles[@]}"; do + for profile in "${current_profiles[@]:-}"; do profile=$(echo "$profile" | tr -d '[:space:]') [[ -z "$profile" ]] && continue @@ -552,9 +552,9 @@ build_docker_image() { local docker_profiles=() local python_only_profiles=("python" "ml" "datascience") - for profile in "${current_profiles[@]}"; do + for profile in "${current_profiles[@]:-}"; do local is_python_only=false - for py_profile in "${python_only_profiles[@]}"; do + for py_profile in "${python_only_profiles[@]:-}"; do if [[ "$profile" == "$py_profile" ]]; then is_python_only=true break From 5ea02a3d3e72f33ea11266bc0d943fd58252e34f Mon Sep 17 00:00:00 2001 From: Andrew Megli Date: Wed, 24 Sep 2025 09:41:30 -0500 Subject: [PATCH 7/8] Replace awk with sed to avoid multi-line errors with older bash (or at least MacOS). Taken from @jeff-r-skillrev - https://github.com/RchGrav/claudebox/pull/77#issuecomment-3325703332 --- main.sh | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/main.sh b/main.sh index 5194a87..92454e1 100755 --- a/main.sh +++ b/main.sh @@ -580,28 +580,27 @@ build_docker_image() { # Build labels local project_folder_name project_folder_name=$(generate_parent_folder_name "$PROJECT_DIR") - local labels="\ -LABEL claudebox.profiles=\"$profile_hash\" -LABEL claudebox.profiles.crc=\"$profiles_file_hash\" -LABEL claudebox.project=\"$project_folder_name\"" - - # Replace placeholders in the project template + local final_dockerfile="$base_dockerfile" - # Replace WHOLE lines that contain the placeholders (with optional spaces) - local final_dockerfile - final_dockerfile=$(awk -v pi="$profile_installations" -v lbs="$labels" ' - # If the whole line is {{ PROFILE_INSTALLATIONS }}, print injected block and skip - /^[[:space:]]*\{\{[[:space:]]*PROFILE_INSTALLATIONS[[:space:]]*\}\}[[:space:]]*$/ { print pi; next } - # If the whole line is {{ LABELS }}, print labels block and skip - /^[[:space:]]*\{\{[[:space:]]*LABELS[[:space:]]*\}\}[[:space:]]*$/ { print lbs; next } - # Otherwise, print the line unchanged - { print } - ' <<<"$base_dockerfile") || error "Failed to apply Dockerfile substitutions" + # First replace the profile installations + final_dockerfile=$(sed -e "/^[[:space:]]*{{[[:space:]]*PROFILE_INSTALLATIONS[[:space:]]*}}[[:space:]]*$/c\\ +$profile_installations" <<<"$final_dockerfile") + + # Then replace the labels section - each on a separate line + final_dockerfile=$(sed -e "/^[[:space:]]*{{[[:space:]]*LABELS[[:space:]]*}}[[:space:]]*$/c\\ +LABEL claudebox.profiles=\"$profile_hash\"\\ +LABEL claudebox.profiles.crc=\"$profiles_file_hash\"\\ +LABEL claudebox.project=\"$project_folder_name\"" <<<"$final_dockerfile") + + # Check if substitution was successful + if [[ $? -ne 0 ]]; then + error "Failed to apply Dockerfile substitutions" + fi # Guard: ensure no unreplaced placeholders remain if grep -q '{{PROFILE_INSTALLATIONS}}' <<<"$final_dockerfile" grep -q '{{LABELS}}' <<<"$final_dockerfile"; then - error "Unreplaced placeholders remain in generated Dockerfile" + error "Unreplaced placeholders remain in generated Dockerfile" fi printf '%s' "$final_dockerfile" > "$dockerfile" From 1d88e43c65cf085ac02dd9da663f485aaf07d2a0 Mon Sep 17 00:00:00 2001 From: Andrew Megli Date: Wed, 24 Sep 2025 09:53:53 -0500 Subject: [PATCH 8/8] Remove "local" declarations to prevent scope issues when "trap" executes. --- lib/docker.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/docker.sh b/lib/docker.sh index 2efd8da..74e0ff6 100755 --- a/lib/docker.sh +++ b/lib/docker.sh @@ -287,8 +287,8 @@ run_claudebox_container() { fi } - local user_mcp_file="" - local project_mcp_file="" + user_mcp_file="" + project_mcp_file="" # Track all temporary MCP files for cleanup declare -a mcp_temp_files=()