-
-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathinit_runestone.sh
More file actions
1262 lines (1062 loc) · 40 KB
/
init_runestone.sh
File metadata and controls
1262 lines (1062 loc) · 40 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
################################################################################
# Runestone First-Time Setup Wizard
################################################################################
#
# This script automates the initialization of a Runestone server for first-time
# users. It guides you through:
# - Validating prerequisites (Docker, Git)
# - Configuring the .env file with BOOK_PATH
# - Pulling Docker images and starting services
# - Initializing the database
# - Optionally adding and building your first book
# - Optionally creating a course
#
# REQUIREMENTS:
# - Docker Desktop with Docker Compose 2.20.2+ (current: 2.38.2)
# - Git (only required if you want to clone book repositories)
# - For Windows: WSL2 with this script run from WSL terminal
#
# USAGE:
# Standalone (one-line install - no repo clone needed):
# bash <(curl -fsSL https://raw.githubusercontent.com/RunestoneInteractive/rs/main/init_runestone.sh)
#
# Traditional (from cloned repo):
# git clone https://github.com/RunestoneInteractive/rs.git
# cd rs
# ./init_runestone.sh
#
# NOTE: Standalone mode downloads only configuration files (docker-compose.yml,
# sample.env). Application code runs inside pre-built Docker images from
# ghcr.io. Files are created in your current working directory.
#
# The script will prompt you for required information and guide you through
# each step of the setup process.
#
################################################################################
set -e # Exit on error
# Color codes for output formatting
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly CYAN='\033[0;36m'
readonly BOLD='\033[1m'
readonly NC='\033[0m' # No Color
# Log file for debugging
readonly LOG_FILE="init_runestone.log"
# Platform detection globals
IS_WSL=false
IS_MACOS=false
IS_LINUX=false
################################################################################
# Utility Functions
################################################################################
# Print functions for wizard-style output
print_header() {
echo -e "\n${BOLD}${BLUE}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}${BLUE} $1${NC}"
echo -e "${BOLD}${BLUE}═══════════════════════════════════════════════════════════════${NC}\n"
}
print_step() {
echo -e "${CYAN}${BOLD}==>${NC} ${BOLD}$1${NC}"
}
print_success() {
echo -e "${GREEN}[OK]${NC} $1"
}
print_error() {
echo -e "${RED}[X] ERROR:${NC} $1" >&2
}
print_warning() {
echo -e "${YELLOW}[!] WARNING:${NC} $1"
}
print_info() {
echo -e "${BLUE}[i]${NC} $1"
}
# Log to file and console
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# Prompt for yes/no with default
prompt_yes_no() {
local prompt="$1"
local default="${2:-y}"
local response
if [[ "$default" == "y" ]]; then
prompt="$prompt [Y/n]: "
else
prompt="$prompt [y/N]: "
fi
read -r -p "$(echo -e ${BOLD}${prompt}${NC})" response
response="${response:-$default}"
[[ "$response" =~ ^[Yy]$ ]]
}
# Prompt for numbered menu choice
prompt_menu() {
local prompt="$1"
local -n result_var=$2 # nameref to output variable
shift 2
local options=("$@")
local user_choice
while true; do
read -r -p "$(echo -e ${BOLD}${prompt}${NC})" user_choice
# Validate choice is a number within range
if [[ "$user_choice" =~ ^[0-9]+$ ]] && [ "$user_choice" -ge 1 ] && [ "$user_choice" -le "${#options[@]}" ]; then
result_var=$user_choice
return 0
else
echo "Invalid choice. Please enter a number between 1 and ${#options[@]}."
fi
done
}
# Prompt for input with optional default (using nameref for return value)
prompt_input() {
local prompt="$1"
local default="$2"
local -n result_var=$3 # nameref to output variable
local response
if [[ -n "$default" ]]; then
prompt="$prompt [$default]: "
else
prompt="$prompt: "
fi
read -r -p "$(echo -e ${BOLD}${prompt}${NC})" response
result_var="${response:-$default}"
}
################################################################################
# File Management
################################################################################
# Download a file from GitHub's raw content
download_file_from_github() {
local filename="$1"
local target_path="$2"
local base_url="https://raw.githubusercontent.com/RunestoneInteractive/rs/main"
local file_url="${base_url}/${filename}"
log "Attempting to download ${filename} from ${file_url}"
# Try curl first
if command -v curl &> /dev/null; then
if curl -fsSL "${file_url}" -o "${target_path}" 2>> "$LOG_FILE"; then
# Verify file was downloaded and is not empty
if [[ -s "${target_path}" ]]; then
log "Successfully downloaded ${filename} using curl"
return 0
else
log "Downloaded file ${filename} is empty"
rm -f "${target_path}"
return 1
fi
else
log "Failed to download ${filename} using curl"
fi
fi
# Fallback to wget
if command -v wget &> /dev/null; then
if wget -qO "${target_path}" "${file_url}" 2>> "$LOG_FILE"; then
# Verify file was downloaded and is not empty
if [[ -s "${target_path}" ]]; then
log "Successfully downloaded ${filename} using wget"
return 0
else
log "Downloaded file ${filename} is empty"
rm -f "${target_path}"
return 1
fi
else
log "Failed to download ${filename} using wget"
fi
fi
# Both methods failed
log "Failed to download ${filename} - no working download tool"
return 1
}
# Ensure required configuration files exist
ensure_required_files() {
local has_compose=false
local has_sample=false
local in_standalone_mode=false
# Check if files already exist
if [[ -f docker-compose.yml ]]; then
has_compose=true
log "Found existing docker-compose.yml"
fi
if [[ -f sample.env ]]; then
has_sample=true
log "Found existing sample.env"
fi
# If both files exist, we're in traditional mode
if $has_compose && $has_sample; then
print_info "Configuration files found - running in traditional mode"
log "Running in traditional mode (files exist)"
return 0
fi
# At least one file is missing - enter standalone mode
in_standalone_mode=true
log "Entering standalone mode - downloading missing files"
echo ""
print_header "Standalone Mode"
print_info "Running in standalone mode - configuration files will be downloaded"
echo ""
echo -e "Files will be created in: ${BOLD}$(pwd)${NC}"
echo ""
echo "The following files will be downloaded from the official Runestone repository:"
if ! $has_compose; then
echo " - docker-compose.yml (Docker service configuration)"
fi
if ! $has_sample; then
echo " - sample.env (Environment variable template)"
fi
echo ""
print_info "Your Runestone server will run using pre-built Docker images"
print_info "No source code repository clone is required"
echo ""
# Download missing files
local download_failed=false
if ! $has_compose; then
print_step "Downloading docker-compose.yml..."
if download_file_from_github "docker-compose.yml" "./docker-compose.yml"; then
print_success "Downloaded docker-compose.yml"
else
print_error "Failed to download docker-compose.yml"
download_failed=true
fi
fi
if ! $has_sample; then
print_step "Downloading sample.env..."
if download_file_from_github "sample.env" "./sample.env"; then
print_success "Downloaded sample.env"
else
print_error "Failed to download sample.env"
download_failed=true
fi
fi
# Check if any downloads failed
if $download_failed; then
echo ""
print_error "Failed to download required files"
echo ""
echo "This may be due to:"
echo " - Network connectivity issues"
echo " - GitHub being unavailable"
echo " - Missing curl/wget tools"
echo ""
echo "Manual download instructions:"
echo " curl -fsSL https://raw.githubusercontent.com/RunestoneInteractive/rs/main/docker-compose.yml -o docker-compose.yml"
echo " curl -fsSL https://raw.githubusercontent.com/RunestoneInteractive/rs/main/sample.env -o sample.env"
echo ""
echo "Or clone the repository:"
echo " git clone https://github.com/RunestoneInteractive/rs.git"
echo " cd rs"
echo " ./init_runestone.sh"
echo ""
exit 1
fi
echo ""
print_success "Configuration files downloaded successfully"
echo ""
print_info "You can inspect these files before continuing if needed"
echo ""
log "Standalone mode setup complete"
return 0
}
################################################################################
# Platform Detection
################################################################################
detect_platform() {
print_step "Detecting platform..."
# Primary method: Check for wslinfo command (most reliable for WSL)
if command -v wslinfo &> /dev/null && wslinfo --version &> /dev/null; then
IS_WSL=true
local wsl_version=$(wslinfo --version 2>/dev/null | head -n 1 || echo "unknown")
print_success "Running on Windows WSL"
log "Platform: Windows WSL (detected via: wslinfo --version: $wsl_version)"
# Fallback methods for older WSL versions or edge cases
elif grep -qi microsoft /proc/version 2>/dev/null || \
grep -qi wsl /proc/version 2>/dev/null || \
[[ -n "${WSL_DISTRO_NAME}" ]] || \
[[ -n "${WSLENV}" ]] || \
[[ "$(uname -r)" == *Microsoft* ]] || \
[[ "$(uname -r)" == *microsoft* ]] || \
[[ "$(uname -r)" == *WSL* ]]; then
IS_WSL=true
print_success "Running on Windows WSL"
log "Platform: Windows WSL (detected via: fallback methods - /proc/version, WSL_DISTRO_NAME, WSLENV, or uname)"
elif [[ "$OSTYPE" == "darwin"* ]]; then
IS_MACOS=true
print_success "Running on macOS"
log "Platform: macOS"
elif [[ "$OSTYPE" == "linux-gnu"* ]] || [[ "$OSTYPE" == "linux" ]]; then
IS_LINUX=true
print_success "Running on Linux"
log "Platform: Linux"
else
print_error "Unknown platform: $OSTYPE"
log "Platform detection failed: OSTYPE=$OSTYPE, uname=$(uname -a)"
exit 1
fi
}
################################################################################
# Prerequisite Validation
################################################################################
check_docker() {
print_step "Checking Docker installation..."
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed or not in PATH"
echo ""
echo "Please install Docker Desktop:"
echo " https://docs.docker.com/compose/install/"
echo ""
if $IS_WSL; then
echo "For Windows WSL: Install Docker Desktop for Windows"
echo " Make sure WSL integration is enabled in Docker Desktop settings"
fi
exit 1
fi
# Check if Docker daemon is running
if ! docker info &> /dev/null; then
print_error "Docker daemon is not running"
echo ""
echo "Please start Docker Desktop and try again"
exit 1
fi
print_success "Docker is installed and running"
log "Docker check: OK"
}
check_docker_compose() {
print_step "Checking Docker Compose version..."
if ! docker compose version &> /dev/null; then
print_error "Docker Compose (v2) is not available"
echo ""
echo "Please install Docker Compose 2.20.2 or later:"
echo " https://docs.docker.com/compose/install/"
exit 1
fi
local compose_version
compose_version=$(docker compose version --short 2>/dev/null || echo "0.0.0")
print_success "Docker Compose version: $compose_version"
log "Docker Compose version: $compose_version"
# Check minimum version (2.20.2)
local min_version="2.20.2"
if ! printf '%s\n' "$min_version" "$compose_version" | sort -V -C 2>/dev/null; then
print_warning "Docker Compose version is older than recommended ($min_version)"
echo " Current: $compose_version"
echo " Consider updating to the latest version"
echo ""
if ! prompt_yes_no "Continue anyway?"; then
echo "Exiting..."
exit 1
fi
fi
}
check_docker_group() {
# Only check on native Linux (not WSL, not macOS)
# Docker Desktop on WSL and macOS handles permissions differently
if ! $IS_LINUX || $IS_WSL; then
log "Skipping docker group check (not native Linux)"
return 0
fi
print_step "Checking Docker group membership..."
# Check if user is in docker group
if groups | grep -q docker || id -nG | grep -q docker; then
print_success "User is in docker group"
log "Docker group check: OK"
return 0
fi
# User is not in docker group
print_warning "You are not in the docker group"
echo ""
echo "Without docker group membership, you may need to run Docker commands with sudo."
echo ""
echo "To add yourself to the docker group, run these commands:"
echo -e " ${CYAN}sudo usermod -aG docker \$USER${NC}"
echo -e " ${CYAN}newgrp docker${NC}"
echo ""
echo "Or log out and back in for the change to take effect."
echo ""
log "Docker group check: WARNING - user not in docker group"
# Non-fatal - allow user to continue with sudo if they want
if ! prompt_yes_no "Continue anyway?" "y"; then
echo ""
echo "Please add yourself to the docker group and run this script again."
exit 1
fi
echo ""
}
check_git() {
print_step "Checking Git installation..."
if ! command -v git &> /dev/null; then
print_error "Git is not installed or not in PATH"
echo ""
log "Git check: FAILED - not installed"
return 1
fi
local git_version
git_version=$(git --version | cut -d' ' -f3)
print_success "Git version: $git_version"
log "Git version: $git_version"
return 0
}
validate_prerequisites() {
print_header "Validating Prerequisites"
detect_platform
BASH_VERSION=$(bash --version | head -n 1 | awk '{print $4}')
if [[ "$BASH_VERSION" < "5.0" ]]; then
print_warning "You are using an old version of Bash ($BASH_VERSION). It is recommended to install a newer version via Homebrew: brew install bash then rerun the script with the new bash: /usr/local/bin/bash init_runestone.sh"
exit
fi
check_docker
check_docker_compose
check_docker_group
ensure_required_files
print_success "All prerequisites validated"
echo ""
}
################################################################################
# Environment Configuration
################################################################################
backup_env() {
if [[ -f .env ]]; then
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file=".env.backup.$timestamp"
print_step "Backing up existing .env file..."
cp .env "$backup_file"
print_success "Backed up to: $backup_file"
log "Backed up .env to $backup_file"
fi
}
create_env_from_sample() {
print_step "Creating .env file from sample.env..."
if [[ ! -f sample.env ]]; then
print_error "sample.env not found in current directory"
echo "Please run this script from the rs directory"
exit 1
fi
cp sample.env .env
# Add default values for optional variables to suppress Docker Compose warnings
cat >> .env << 'EOF'
# Optional variables (set to empty to suppress Docker Compose warnings)
HOSTNAME=
LOAD_BALANCER_HOST=
SPACES_KEY=
SPACES_SECRET=
EMAIL_SENDER=
EMAIL_SERVER=
EMAIL_LOGIN=
EOF
print_success "Created .env file"
log "Created .env from sample.env with optional variable defaults"
}
convert_windows_path() {
local input_path="$1"
local -n output_var=$2 # nameref to output variable
log "convert_windows_path called with: $input_path"
log "IS_WSL=$IS_WSL, IS_LINUX=$IS_LINUX, IS_MACOS=$IS_MACOS"
# Check if it looks like a Windows path
# Pattern 1: C:\path or C:/path (with slash after colon)
# Pattern 2: C:path (backslashes were consumed by bash - no slash after colon)
if [[ "$input_path" =~ ^[A-Za-z]: ]]; then
log "Windows path pattern detected"
# Extract drive letter (first character) and convert to lowercase using bash
local drive_letter="${input_path:0:1}"
drive_letter="${drive_letter,,}" # Bash 4+ lowercase conversion
# Extract the rest of the path (after drive letter and colon)
local rest_of_path="${input_path:2}"
# Remove leading slash or backslash if present
rest_of_path="${rest_of_path#/}"
rest_of_path="${rest_of_path#\\}"
# Replace all backslashes with forward slashes
rest_of_path="${rest_of_path//\\//}"
# Remove any duplicate slashes
rest_of_path="${rest_of_path//\/\//\/}"
# Construct WSL path
local wsl_path="/mnt/${drive_letter}/${rest_of_path}"
# Remove trailing slash if present
wsl_path="${wsl_path%/}"
log "Converted to WSL path: $wsl_path"
echo ""
print_info "Detected Windows path - converting to WSL format:"
echo " Windows: $input_path"
echo " WSL: $wsl_path"
echo ""
if ! $IS_WSL; then
print_warning "Not running in WSL - path conversion may not work correctly"
echo " For Windows, please run this script from a WSL terminal"
echo " Current platform: IS_WSL=$IS_WSL, IS_LINUX=$IS_LINUX, IS_MACOS=$IS_MACOS"
echo ""
fi
# Set output variable to converted path
output_var="$wsl_path"
else
# Not a Windows path, return as-is
log "Not a Windows path, returning as-is: $input_path"
output_var="$input_path"
fi
}
prompt_for_book_path() {
local -n result_var=$1 # nameref to output variable
print_step "Configuring BOOK_PATH..."
echo ""
echo "BOOK_PATH is the directory where your Runestone books will be stored."
echo "This should be a path on your host machine (not inside Docker)."
echo ""
if $IS_WSL; then
echo "You can provide paths in either format:"
echo " Windows format: C:\\Projects\\runestone\\books"
echo " WSL format: /mnt/c/Projects/runestone/books"
echo " Linux format: /home/username/runestone/books"
echo ""
print_info "Windows paths will be automatically converted to WSL format"
echo ""
elif $IS_LINUX || $IS_MACOS; then
echo "Examples:"
echo " /home/username/runestone/books (Linux)"
echo " /Users/username/runestone/books (macOS)"
echo " ~/runestone/books (relative to home)"
echo ""
fi
local user_input_path
prompt_input "Enter the full path to your books directory" "" user_input_path
if [[ -z "$user_input_path" ]]; then
print_error "BOOK_PATH cannot be empty"
exit 1
fi
# Convert Windows paths to WSL format automatically
local original_path="$user_input_path"
local converted_path
convert_windows_path "$user_input_path" converted_path
# Expand ~ to $HOME if present
converted_path="${converted_path/#\~/$HOME}"
# Log the final path for debugging
log "BOOK_PATH after conversion: $converted_path (original: $original_path)"
# Check if directory exists
if [[ ! -d "$converted_path" ]]; then
print_warning "Directory does not exist: $converted_path"
# If it was a Windows path, provide helpful context
if [[ "$original_path" != "$converted_path" ]]; then
echo ""
echo "The Windows path was converted to: $converted_path"
echo "This directory will be created in WSL and should map to your Windows directory."
echo ""
fi
if prompt_yes_no "Create this directory?"; then
if mkdir -p "$converted_path" 2>/dev/null; then
print_success "Created directory: $converted_path"
log "Created BOOK_PATH: $converted_path"
# Verify it's accessible
if [[ ! -w "$converted_path" ]]; then
print_warning "Directory created but may not be writable"
echo " You may need to check permissions"
fi
else
print_error "Failed to create directory: $converted_path"
echo ""
echo "Possible issues:"
echo " - Invalid path format"
echo " - Insufficient permissions"
if [[ "$original_path" =~ ^[A-Za-z]:[/\\] ]]; then
echo " - For Windows paths, the drive must be accessible in WSL"
echo " - Try accessing the drive first: cd /mnt/c"
fi
exit 1
fi
else
print_error "BOOK_PATH must exist. Please create it and run the script again."
exit 1
fi
else
print_success "Directory exists: $converted_path"
log "Verified BOOK_PATH exists: $converted_path"
fi
# Set the result via nameref
result_var="$converted_path"
}
update_env_file() {
local book_path="$1"
print_step "Updating .env file with BOOK_PATH..."
# Use sed to replace the BOOK_PATH line
# This handles both commented and uncommented lines
if grep -q "^BOOK_PATH=" .env; then
# Replace existing uncommented line
sed -i.bak "s|^BOOK_PATH=.*|BOOK_PATH=$book_path|" .env
elif grep -q "^#.*BOOK_PATH=" .env; then
# Uncomment and replace
sed -i.bak "s|^#.*BOOK_PATH=.*|BOOK_PATH=$book_path|" .env
else
# Add new line
echo "BOOK_PATH=$book_path" >> .env
fi
rm -f .env.bak
print_success "Updated BOOK_PATH in .env"
log "Set BOOK_PATH=$book_path in .env"
}
configure_environment() {
print_header "Configuring Environment"
# Show detected platform for debugging
log "configure_environment: IS_WSL=$IS_WSL, IS_LINUX=$IS_LINUX, IS_MACOS=$IS_MACOS"
backup_env
create_env_from_sample
local book_path
prompt_for_book_path book_path
update_env_file "$book_path"
echo ""
print_success "Environment configuration complete"
echo ""
}
################################################################################
# Docker Service Initialization
################################################################################
pull_images() {
print_step "Pulling Docker images..."
echo ""
print_info "This may take several minutes on first run..."
echo ""
if docker compose pull; then
print_success "Docker images pulled successfully"
log "Docker images pulled"
else
print_error "Failed to pull Docker images"
exit 1
fi
}
start_database() {
print_step "Starting database service..."
if docker compose up -d db; then
print_success "Database service started"
log "Database service started"
else
print_error "Failed to start database service"
exit 1
fi
# Wait for database to be ready
print_info "Waiting for database to be ready..."
sleep 5
# Try to check if db is healthy (with timeout)
local max_attempts=30
local attempt=0
while [ $attempt -lt $max_attempts ]; do
if docker compose exec -T db pg_isready -U runestone &> /dev/null; then
print_success "Database is ready"
log "Database ready after $attempt attempts"
return 0
fi
attempt=$((attempt + 1))
sleep 1
echo -n "."
done
echo ""
print_warning "Could not verify database status, proceeding anyway..."
}
init_database() {
print_step "Initializing database..."
echo ""
# Check if database is already initialized by checking for the boguscourse
print_info "Checking if database is already initialized..."
if docker compose exec -T db psql -U runestone -d runestone_dev -tAc "SELECT 1 FROM courses WHERE course_name='boguscourse' LIMIT 1;" 2>/dev/null | grep -q "1"; then
echo ""
print_warning "Database appears to be already initialized (test data exists)"
echo ""
echo "Please choose an option:"
echo " 1. Skip initialization and continue"
echo " 2. Reset database (WARNING: destroys all data)"
echo " 3. Exit script"
echo ""
local choice
prompt_menu "Enter your choice (1-3): " choice "Skip initialization" "Reset database" "Exit"
case $choice in
1)
print_success "Skipping database initialization"
log "Database initialization skipped - already initialized"
return 0
;;
2)
print_step "Resetting database..."
echo ""
print_warning "This will destroy all existing data!"
if prompt_yes_no "Are you sure you want to reset the database?" "n"; then
print_info "Stopping services and removing volumes..."
docker compose down -v
echo ""
print_info "Starting database service..."
docker compose up -d db
echo ""
print_info "Waiting for database to be ready..."
sleep 5
# Continue to initialization below
print_success "Database reset complete"
log "Database reset and ready for initialization"
else
print_error "Database reset cancelled. Exiting."
exit 1
fi
;;
3)
echo ""
print_info "Exiting script. You can manually handle the database with:"
echo " docker compose down -v # Remove volumes"
echo " docker compose up -d db # Restart database"
echo " ./init_runestone.sh # Re-run this script"
echo ""
exit 0
;;
esac
fi
print_info "Creating tables and test users..."
echo ""
if docker compose run --rm rsmanage rsmanage initdb; then
print_success "Database initialized successfully"
log "Database initialized"
else
print_error "Failed to initialize database"
echo ""
print_info "If you see 'duplicate key' errors, the database may already be initialized."
print_info "Run: docker compose down -v # to reset and start fresh"
exit 1
fi
}
start_all_services() {
print_step "Starting all Runestone services..."
echo ""
if docker compose up -d; then
print_success "All services started"
log "All services started"
else
print_error "Failed to start services"
exit 1
fi
# Give services time to start
print_info "Waiting for services to initialize..."
sleep 5
}
verify_server_running() {
print_step "Verifying server is running..."
local max_attempts=30
local attempt=0
while [ $attempt -lt $max_attempts ]; do
if curl -s -f http://localhost > /dev/null 2>&1; then
print_success "Server is responding at http://localhost"
log "Server verified running"
return 0
fi
attempt=$((attempt + 1))
sleep 1
echo -n "."
done
echo ""
print_warning "Could not verify server is responding"
print_info "Check logs with: docker compose logs -f"
}
initialize_services() {
print_header "Initializing Docker Services"
pull_images
echo ""
start_database
echo ""
init_database
echo ""
start_all_services
echo ""
verify_server_running
echo ""
}
################################################################################
# Book Management
################################################################################
prompt_add_book() {
echo ""
print_header "Book Setup (Optional)"
echo ""
echo "Would you like to add a book to your Runestone server now?"
echo "You can also do this later using: docker compose run --rm rsmanage rsmanage addbookauthor"
echo ""
prompt_yes_no "Add a book now?" "y"
}
prompt_book_details() {
local -n repo_result=$1 # nameref for book_repo
local -n name_result=$2 # nameref for book_name
print_step "Book Details"
echo ""
# Provide suggestions for common books
echo "Popular Runestone books:"
echo " - overview: https://github.com/RunestoneInteractive/overview.git"
echo " - thinkcspy: https://github.com/RunestoneInteractive/thinkcspy.git"
echo " - pythonds: https://github.com/RunestoneInteractive/pythonds.git"
echo ""
local input_repo
prompt_input "Enter the Git repository URL for the book" "" input_repo
if [[ -z "$input_repo" ]]; then
print_error "Repository URL cannot be empty"
return 1
fi
# Try to extract book name from repo URL (e.g., overview from overview.git)
# Use bash parameter expansion instead of basename
local suggested_name
suggested_name="${input_repo##*/}" # Remove everything up to last /
suggested_name="${suggested_name%.git}" # Remove .git extension
echo ""
local input_name
prompt_input "Enter the book name/document-id (basecourse)" "$suggested_name" input_name
if [[ -z "$input_name" ]]; then
print_error "Book name cannot be empty"
return 1
fi
# Set results via nameref
repo_result="$input_repo"
name_result="$input_name"
}
clone_book() {
local book_repo="$1"
local book_path="$2"
local -n cloned_name_result=$3 # nameref for result
print_step "Cloning book repository..."
echo ""
# Get book name from repo using bash parameter expansion
local book_name
book_name="${book_repo##*/}" # Remove everything up to last /
book_name="${book_name%.git}" # Remove .git extension
local target_dir="$book_path/$book_name"
if [[ -d "$target_dir" ]]; then
print_warning "Directory already exists: $target_dir"
if ! prompt_yes_no "Skip cloning and use existing directory?"; then
return 1
fi
print_info "Using existing directory"
else
if git clone "$book_repo" "$target_dir"; then
print_success "Book cloned to: $target_dir"
log "Cloned $book_repo to $target_dir"
else
print_error "Failed to clone repository"
return 1
fi
fi
# Return result via nameref
cloned_name_result="$book_name"
}
add_book_to_db() {
local book_name="$1"
print_step "Adding book to database..."
# Run the addbookauthor command interactively
# Instead of providing the inputs by pipe, use the parameters directly, which is supported by the command and more reliable than simulating interactive input
# using interactive intput would require -T on the run command, which can cause issues with some environments and is less efficient
if docker compose run --rm rsmanage rsmanage addbookauthor --book "${book_name}" --author "testuser1"; then
print_success "Book added to database"
log "Added book $book_name to database"