crush-sandbox 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,47 @@
2
2
 
3
3
  set -e
4
4
 
5
- VERSION="0.5.2"
5
+ VERSION="0.6.1"
6
6
 
7
7
  # Docker image configuration - can be overridden via DOCKER_SANDBOX_IMAGE environment variable
8
- DOCKER_IMAGE="${DOCKER_SANDBOX_IMAGE:-node:22-alpine}"
8
+ DOCKER_IMAGE="${DOCKER_SANDBOX_IMAGE:-node:22-bookworm}"
9
+
10
+ # Color codes (disabled if NO_COLOR is set or not a TTY)
11
+ if [ -t 0 ] && [ -z "$NO_COLOR" ]; then
12
+ GREEN='\033[0;32m'
13
+ RED='\033[0;31m'
14
+ YELLOW='\033[1;33m'
15
+ CYAN='\033[0;36m'
16
+ GRAY='\033[0;90m'
17
+ NC='\033[0m'
18
+ else
19
+ GREEN=''
20
+ RED=''
21
+ YELLOW=''
22
+ CYAN=''
23
+ GRAY=''
24
+ NC=''
25
+ fi
26
+
27
+ print_success() {
28
+ echo -e "${GREEN}✓${NC} $1"
29
+ }
30
+
31
+ print_error() {
32
+ echo -e "${RED}✗${NC} $1" >&2
33
+ }
34
+
35
+ print_warning() {
36
+ echo -e "${YELLOW}!${NC} $1"
37
+ }
38
+
39
+ print_info() {
40
+ echo -e "${CYAN}${1}${NC}"
41
+ }
42
+
43
+ print_dry_run() {
44
+ echo -e "${YELLOW}[DRY RUN]${NC} $1"
45
+ }
9
46
 
10
47
  print_usage() {
11
48
  echo "Usage: crush-sandbox <command> [options]"
@@ -15,13 +52,15 @@ print_usage() {
15
52
  echo " clean Remove the sandbox container for current workspace"
16
53
  echo " install Install this script to /usr/local/bin"
17
54
  echo " update Update to the latest version"
55
+ echo " uninstall Remove all docker-sandbox-crush artifacts"
18
56
  echo ""
19
57
  echo "Options:"
20
58
  echo " --version Show version information"
21
- echo " --force Skip confirmation for clean command"
59
+ echo " --force Skip confirmation prompts"
22
60
  echo " --shell Start interactive shell instead of Crush CLI (for debugging)"
23
- echo " --no-host-config Skip mounting host Crush config directory"
61
+ echo " --no-host-config Skip mounting host Crush config directory"
24
62
  echo " --cred-scan Enable credential scanning before starting container"
63
+ echo " --dry-run Preview uninstall actions without executing"
25
64
  }
26
65
 
27
66
  validate_docker() {
@@ -760,6 +799,345 @@ update_command() {
760
799
  echo "View release notes: https://github.com/wireless25/crush-sandbox/releases"
761
800
  }
762
801
 
802
+ uninstall_command() {
803
+ local dry_run="$1"
804
+ local force="$2"
805
+
806
+ # Track failures for summary
807
+ local script_failed=false
808
+ local alias_failed=false
809
+ local container_failures=()
810
+ local volume_failures=()
811
+
812
+ echo "=== Docker Sandbox Crush Uninstall ==="
813
+ echo ""
814
+
815
+ # Validate Docker is available (even for dry-run, to detect containers)
816
+ if ! validate_docker; then
817
+ echo "Error: Docker is required to uninstall containers and volumes" >&2
818
+ exit 1
819
+ fi
820
+
821
+ # Detect running containers
822
+ local running_containers
823
+ running_containers=$(docker ps --filter "name=^crush-sandbox-" --format '{{.Names}}' 2>/dev/null || echo "")
824
+
825
+ if [ -n "$running_containers" ]; then
826
+ local count
827
+ count=$(echo "$running_containers" | wc -l | tr -d ' ')
828
+ print_warning "$count running container(s) found:"
829
+ echo "$running_containers" | while read -r container; do
830
+ echo " - $container"
831
+ done
832
+ echo ""
833
+
834
+ if [ "$dry_run" = "true" ]; then
835
+ print_dry_run "Would continue with uninstall despite running containers"
836
+ else
837
+ read -p "Continue anyway? [y/N] " confirm
838
+ if [[ ! "$confirm" =~ ^[yY] ]]; then
839
+ echo "Uninstall cancelled"
840
+ exit 2
841
+ fi
842
+ fi
843
+ fi
844
+
845
+ # Step 1: Remove script
846
+ local script_paths=("/usr/local/bin/crush-sandbox" "/usr/local/bin/docker-sandbox-crush")
847
+ local script_to_remove=""
848
+ for script_path in "${script_paths[@]}"; do
849
+ if [ -f "$script_path" ]; then
850
+ script_to_remove="$script_path"
851
+ break
852
+ fi
853
+ done
854
+
855
+ if [ -n "$script_to_remove" ]; then
856
+ print_info "Script: $script_to_remove"
857
+
858
+ if [ "$dry_run" = "true" ]; then
859
+ print_dry_run "Would remove script at $script_to_remove"
860
+ elif [ "$force" = "true" ]; then
861
+ if sudo rm -f "$script_to_remove" 2>/dev/null; then
862
+ print_success "Removed script at $script_to_remove"
863
+ else
864
+ print_error "Failed to remove script at $script_to_remove"
865
+ script_failed=true
866
+ fi
867
+ else
868
+ read -p "Remove script at $script_to_remove? [y/N] " confirm
869
+ if [[ "$confirm" =~ ^[yY] ]]; then
870
+ if sudo rm -f "$script_to_remove" 2>/dev/null; then
871
+ print_success "Removed script at $script_to_remove"
872
+ else
873
+ print_error "Failed to remove script at $script_to_remove"
874
+ script_failed=true
875
+ fi
876
+ else
877
+ echo "Skipped script removal"
878
+ fi
879
+ fi
880
+ else
881
+ echo "No script found (expected at /usr/local/bin/crush-sandbox or /usr/local/bin/docker-sandbox-crush)"
882
+ fi
883
+
884
+ echo ""
885
+
886
+ # Step 2: Remove alias
887
+ local alias_path="/usr/local/bin/crushbox"
888
+ if [ -f "$alias_path" ] || [ -L "$alias_path" ]; then
889
+ print_info "Alias: $alias_path"
890
+
891
+ if [ "$dry_run" = "true" ]; then
892
+ print_dry_run "Would remove alias at $alias_path"
893
+ elif [ "$force" = "true" ]; then
894
+ if sudo rm -f "$alias_path" 2>/dev/null; then
895
+ print_success "Removed alias at $alias_path"
896
+ else
897
+ print_error "Failed to remove alias at $alias_path"
898
+ alias_failed=true
899
+ fi
900
+ else
901
+ read -p "Remove alias at $alias_path? [y/N] " confirm
902
+ if [[ "$confirm" =~ ^[yY] ]]; then
903
+ if sudo rm -f "$alias_path" 2>/dev/null; then
904
+ print_success "Removed alias at $alias_path"
905
+ else
906
+ print_error "Failed to remove alias at $alias_path"
907
+ alias_failed=true
908
+ fi
909
+ else
910
+ echo "Skipped alias removal"
911
+ fi
912
+ fi
913
+ else
914
+ echo "No alias found at $alias_path"
915
+ fi
916
+
917
+ echo ""
918
+
919
+ # Step 3: Remove containers
920
+ local containers
921
+ containers=$(docker ps -a --filter "name=^crush-sandbox-" --format '{{.Names}}' 2>/dev/null || echo "")
922
+
923
+ if [ -n "$containers" ]; then
924
+ local container_count
925
+ container_count=$(echo "$containers" | wc -l | tr -d ' ')
926
+ echo "Found $container_count container(s):"
927
+
928
+ # Use a different approach to avoid subshell issues
929
+ while IFS= read -r container; do
930
+ local status
931
+ status=$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || echo "unknown")
932
+ echo " - $container ($status)"
933
+ done <<< "$containers"
934
+
935
+ if [ "$dry_run" = "true" ]; then
936
+ print_dry_run "Would remove $container_count container(s)"
937
+ echo ""
938
+ elif [ "$force" = "true" ] || [[ "$(read -p "Remove $container_count container(s)? [y/N] " confirm; echo "$confirm")" =~ ^[yY] ]]; then
939
+ if [ "$force" != "true" ]; then
940
+ read -p "Remove $container_count container(s)? [y/N] " confirm
941
+ if [[ ! "$confirm" =~ ^[yY] ]]; then
942
+ echo "Skipped container removal"
943
+ else
944
+ echo ""
945
+ fi
946
+ else
947
+ echo ""
948
+ fi
949
+
950
+ if [ "$force" = "true" ] || [[ "$confirm" =~ ^[yY] ]]; then
951
+ local i=1
952
+ while IFS= read -r container; do
953
+ echo -n "[$i/$container_count] Removing container $container... "
954
+ if docker stop "$container" >/dev/null 2>&1; then
955
+ if docker rm "$container" >/dev/null 2>&1; then
956
+ print_success "Removed $container"
957
+ else
958
+ print_error "Failed to remove $container (stop succeeded)"
959
+ container_failures+=("Failed to remove $container")
960
+ fi
961
+ else
962
+ print_error "Failed to stop $container"
963
+ container_failures+=("Failed to stop $container")
964
+ fi
965
+ i=$((i + 1))
966
+ done <<< "$containers"
967
+ fi
968
+ else
969
+ echo "Skipped container removal"
970
+ fi
971
+ else
972
+ echo "No containers found"
973
+ fi
974
+
975
+ echo ""
976
+
977
+ # Step 4: Remove volumes
978
+ local volumes
979
+ volumes=$(docker volume ls --filter "name=^crush-cache-" --format '{{.Name}}' 2>/dev/null || echo "")
980
+
981
+ if [ -n "$volumes" ]; then
982
+ local volume_count
983
+ volume_count=$(echo "$volumes" | wc -l | tr -d ' ')
984
+ echo "Found $volume_count volume(s):"
985
+
986
+ # Use a different approach to avoid subshell issues
987
+ while IFS= read -r volume; do
988
+ local size
989
+ size=$(docker volume inspect --format '{{.UsageData.Size}}' "$volume" 2>/dev/null || echo "unknown")
990
+ echo " - $volume ($size)"
991
+ done <<< "$volumes"
992
+
993
+ if [ "$dry_run" = "true" ]; then
994
+ print_dry_run "Would remove $volume_count volume(s)"
995
+ echo ""
996
+ elif [ "$force" = "true" ] || [[ "$(read -p "Remove $volume_count volume(s)? [y/N] " confirm; echo "$confirm")" =~ ^[yY] ]]; then
997
+ if [ "$force" != "true" ]; then
998
+ read -p "Remove $volume_count volume(s)? [y/N] " confirm
999
+ if [[ ! "$confirm" =~ ^[yY] ]]; then
1000
+ echo "Skipped volume removal"
1001
+ else
1002
+ echo ""
1003
+ fi
1004
+ else
1005
+ echo ""
1006
+ fi
1007
+
1008
+ if [ "$force" = "true" ] || [[ "$confirm" =~ ^[yY] ]]; then
1009
+ local i=1
1010
+ while IFS= read -r volume; do
1011
+ echo -n "[$i/$volume_count] Removing volume $volume... "
1012
+ if docker volume rm "$volume" >/dev/null 2>&1; then
1013
+ print_success "Removed $volume"
1014
+ else
1015
+ print_error "Failed to remove $volume"
1016
+ volume_failures+=("Failed to remove $volume")
1017
+ fi
1018
+ i=$((i + 1))
1019
+ done <<< "$volumes"
1020
+ fi
1021
+ else
1022
+ echo "Skipped volume removal"
1023
+ fi
1024
+ else
1025
+ echo "No volumes found"
1026
+ fi
1027
+
1028
+ echo ""
1029
+
1030
+ # Final summary
1031
+ echo "=== Uninstall Summary ==="
1032
+
1033
+ # Script status
1034
+ if [ -n "$script_to_remove" ]; then
1035
+ if [ "$dry_run" = "true" ]; then
1036
+ echo -e "Script: ${YELLOW}[DRY RUN]${NC} Would remove $script_to_remove"
1037
+ elif [ "$script_failed" = "true" ]; then
1038
+ echo -e "Script: ${RED}✗ Failed${NC}"
1039
+ else
1040
+ echo -e "Script: ${GREEN}✓ Removed${NC}"
1041
+ fi
1042
+ else
1043
+ echo -e "Script: ${GRAY}N/A${NC} (not installed)"
1044
+ fi
1045
+
1046
+ # Alias status
1047
+ if [ -f "$alias_path" ] || [ -L "$alias_path" ]; then
1048
+ if [ "$dry_run" = "true" ]; then
1049
+ echo -e "Alias: ${YELLOW}[DRY RUN]${NC} Would remove $alias_path"
1050
+ elif [ "$alias_failed" = "true" ]; then
1051
+ echo -e "Alias: ${RED}✗ Failed${NC}"
1052
+ else
1053
+ echo -e "Alias: ${GREEN}✓ Removed${NC}"
1054
+ fi
1055
+ else
1056
+ echo -e "Alias: ${GRAY}N/A${NC} (not installed)"
1057
+ fi
1058
+
1059
+ # Containers status
1060
+ if [ -n "$containers" ]; then
1061
+ local total_containers
1062
+ total_containers=$(echo "$containers" | wc -l | tr -d ' ')
1063
+ local container_fail_count
1064
+ if [ "$dry_run" = "true" ]; then
1065
+ container_fail_count=0
1066
+ echo -e "Containers: ${YELLOW}[DRY RUN]${NC} Would remove $total_containers container(s)"
1067
+ else
1068
+ container_fail_count=${#container_failures[@]}
1069
+ if [ $container_fail_count -eq 0 ]; then
1070
+ echo -e "Containers: ${GREEN}✓ Removed all${NC} ($total_containers)"
1071
+ else
1072
+ echo -e "Containers: ${RED}✗ Partially failed${NC} ($container_fail_count/$total_containers failed)"
1073
+ fi
1074
+ fi
1075
+ else
1076
+ echo -e "Containers: ${GRAY}N/A${NC} (none found)"
1077
+ fi
1078
+
1079
+ # Volumes status
1080
+ if [ -n "$volumes" ]; then
1081
+ local total_volumes
1082
+ total_volumes=$(echo "$volumes" | wc -l | tr -d ' ')
1083
+ local volume_fail_count
1084
+ if [ "$dry_run" = "true" ]; then
1085
+ volume_fail_count=0
1086
+ echo -e "Volumes: ${YELLOW}[DRY RUN]${NC} Would remove $total_volumes volume(s)"
1087
+ else
1088
+ volume_fail_count=${#volume_failures[@]}
1089
+ if [ $volume_fail_count -eq 0 ]; then
1090
+ echo -e "Volumes: ${GREEN}✓ Removed all${NC} ($total_volumes)"
1091
+ else
1092
+ echo -e "Volumes: ${RED}✗ Partially failed${NC} ($volume_fail_count/$total_volumes failed)"
1093
+ fi
1094
+ fi
1095
+ else
1096
+ echo -e "Volumes: ${GRAY}N/A${NC} (none found)"
1097
+ fi
1098
+
1099
+ # Error details
1100
+ if [ "$dry_run" != "true" ]; then
1101
+ local total_errors=0
1102
+ [ "$script_failed" = "true" ] && total_errors=$((total_errors + 1))
1103
+ [ "$alias_failed" = "true" ] && total_errors=$((total_errors + 1))
1104
+ total_errors=$((total_errors + ${#container_failures[@]} + ${#volume_failures[@]}))
1105
+
1106
+ if [ $total_errors -gt 0 ]; then
1107
+ echo ""
1108
+ echo "=== Errors ==="
1109
+
1110
+ if [ "$script_failed" = "true" ]; then
1111
+ echo -e " ${RED}✗${NC} Script: Failed to remove"
1112
+ fi
1113
+
1114
+ if [ "$alias_failed" = "true" ]; then
1115
+ echo -e " ${RED}✗${NC} Alias: Failed to remove"
1116
+ fi
1117
+
1118
+ for failure in "${container_failures[@]}"; do
1119
+ echo -e " ${RED}✗${NC} Container: $failure"
1120
+ done
1121
+
1122
+ for failure in "${volume_failures[@]}"; do
1123
+ echo -e " ${RED}✗${NC} Volume: $failure"
1124
+ done
1125
+
1126
+ echo ""
1127
+ echo -e "${RED}Uninstall complete. $total_errors error(s) occurred.${NC}"
1128
+ exit 1
1129
+ else
1130
+ echo ""
1131
+ echo -e "${GREEN}Uninstall complete. All components removed successfully.${NC}"
1132
+ exit 0
1133
+ fi
1134
+ else
1135
+ echo ""
1136
+ echo -e "${YELLOW}[DRY RUN] Uninstall preview complete. No changes made.${NC}"
1137
+ exit 0
1138
+ fi
1139
+ }
1140
+
763
1141
  # Main script logic
764
1142
  if [ $# -lt 1 ]; then
765
1143
  print_usage >&2
@@ -773,6 +1151,7 @@ SHELL_MODE="false"
773
1151
  SHOW_VERSION="false"
774
1152
  NO_HOST_CONFIG="false"
775
1153
  CRED_SCAN="false"
1154
+ DRY_RUN="false"
776
1155
 
777
1156
  while [[ $# -gt 0 ]]; do
778
1157
  case "$1" in
@@ -796,6 +1175,10 @@ while [[ $# -gt 0 ]]; do
796
1175
  CRED_SCAN="true"
797
1176
  shift
798
1177
  ;;
1178
+ --dry-run)
1179
+ DRY_RUN="true"
1180
+ shift
1181
+ ;;
799
1182
  -*)
800
1183
  echo "Error: Unknown option: $1" >&2
801
1184
  print_usage
@@ -838,6 +1221,9 @@ case "$COMMAND" in
838
1221
  update)
839
1222
  update_command
840
1223
  ;;
1224
+ uninstall)
1225
+ uninstall_command "$DRY_RUN" "$FORCE"
1226
+ ;;
841
1227
  *)
842
1228
  echo "Error: Unknown command: $COMMAND" >&2
843
1229
  print_usage >&2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crush-sandbox",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "Docker sandbox for the Crush CLI with per-workspace caching",
5
5
  "license": "MIT",
6
6
  "bin": {