-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbooth
More file actions
executable file
·1257 lines (1095 loc) · 44.2 KB
/
booth
File metadata and controls
executable file
·1257 lines (1095 loc) · 44.2 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
# Copyright 2025-2026 : Nawa Manusitthipol
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# CodingBooth Wrapper (booth)
# Downloads, verifies, and runs the platform-specific CodingBooth binary.
# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash
set -euo pipefail
trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR
# --- PIPE INSTALL DETECTION ---
# Detect if running via pipe (curl ... | bash)
# When piped, $0 is the shell name, not a script path
if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \
"$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \
"$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then
echo "Installing CodingBooth wrapper..."
curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth
chmod +x booth
echo ""
./booth install
echo ""
echo "✅ CodingBooth has been installed."
echo ""
if grep -qs 'booth()' ~/.bashrc ~/.zshrc ~/.bash_profile ~/.profile 2>/dev/null
# booth shell function already configured — use 'booth' not './booth'
then SCRPT=booth
else SCRPT=./booth
fi
echo "Run '$SCRPT help' to see available commands."
echo ""
echo "Try '$SCRPT example' to see ready to use example."
echo "Or, '$SCRPT config' to quickly start a new project."
if [[ "$SCRPT" == "./booth" ]]; then
echo ""
echo "💡 Tip: Add the following to your shell profile (~/.bashrc or ~/.zshrc)"
echo " to use 'booth' as a command from anywhere:"
echo ""
if [[ "$SHELL" == */bash ]];
then RC_FILE="\$HOME/.bashrc"
else RC_FILE="\$HOME/.zshrc"
fi
echo " ./booth shell-config && source \"$RC_FILE\""
fi
echo ""
exit 0
fi
VERSION=0.11.0
VERBOSE="${VERBOSE:-true}"
# Resolve script directory (follow symlinks) so all project paths are location-based.
get_script_dir() {
local source="${BASH_SOURCE[0]}"
while [[ -L "$source" ]]; do
local dir
dir="$(cd -P "$(dirname "$source")" && pwd)"
source="$(readlink "$source")"
[[ "$source" != /* ]] && source="$dir/$source"
done
cd -P "$(dirname "$source")" && pwd
}
SCRIPT_DIR="$(get_script_dir)"
BOOTH_DIR="${SCRIPT_DIR}/.booth"
TOOLS_DIR="${BOOTH_DIR}/tools"
LOCK_FILE="${TOOLS_DIR}/codingbooth.lock"
# --- NESTED BOOTH DETECTION ---
# Detect if running inside a CodingBooth container and prevent accidental nested execution.
# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port.
detect_nested_booth() {
# Check if we're inside a CodingBooth container
# Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set
if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then
return 0 # Not inside a container, continue normally
fi
# We're inside a CodingBooth container
if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then
cat >&2 <<'EOF'
╔════════════════════════════════════════════════════════════════════════╗
║ Running booth inside a booth container ║
╠════════════════════════════════════════════════════════════════════════╣
║ You appear to be running 'booth' from inside a CodingBooth container. ║
║ This is usually accidental - the booth script is visible here because ║
║ your project folder is mounted at /home/coder/code. ║
║ ║
║ To configure this booth (edit .booth/ settings), run: ║
║ ║
║ booth config ║
║ ║
║ If you intentionally want to run a nested booth (booth-in-booth), set: ║
║ ║
║ export BOOTH_IN_BOOTH=true ║
║ ║
║ AND specify a different port (not the current container's port): ║
║ ║
║ ./booth --port NEXT ... ║
║ ./booth --port 11000 ... ║
║ ║
╚════════════════════════════════════════════════════════════════════════╝
EOF
exit 1
fi
# BOOTH_IN_BOOTH=true is set, now check port
local requested_port=""
local args=("$@")
local i=0
while [[ $i -lt ${#args[@]} ]]; do
case "${args[$i]}" in
--port)
if [[ $((i+1)) -lt ${#args[@]} ]]; then
requested_port="${args[$((i+1))]}"
fi
break
;;
--port=*)
requested_port="${args[$i]#--port=}"
break
;;
esac
((i++)) || true
done
# Get current container's ports
local host_port="${BOOTH_HOST_PORT:-}"
local code_port="${BOOTH_CODE_PORT:-10000}"
# If no port specified, error out - we need an explicit different port
if [[ -z "$requested_port" ]]; then
cat >&2 <<EOF
╔═══════════════════════════════════════════════════════════════════════════╗
║ Port conflict in nested booth ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ BOOTH_IN_BOOTH=true is set, but no port was specified. ║
║ ║
║ You must specify a different port to avoid conflicts with this container: ║
║ Current container's host port: ${host_port:-"(not set)"} ║
║ Current container's code port: ${code_port} ║
║ ║
║ Use one of: ║
║ ./booth --port NEXT ... (auto-find next available port) ║
║ ./booth --port RANDOM ... (use a random port) ║
║ ./booth --port 11000 ... (explicit different port) ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════╝
EOF
exit 1
fi
# Check if the requested port conflicts with current container's ports
# Skip check for NEXT and RANDOM as they will find a different port
if [[ "$requested_port" != "NEXT" && "$requested_port" != "RANDOM" ]]; then
if [[ "$requested_port" == "$host_port" ]]; then
cat >&2 <<EOF
╔═════════════════════════════════════════════════════════════════════════════════╗
║ Port conflict in nested booth ║
╠═════════════════════════════════════════════════════════════════════════════════╣
║ The requested port ($requested_port) conflicts with this container's host port. ║
║ ║
║ Use a different port: ║
║ ./booth --port NEXT ... (auto-find next available port) ║
║ ./booth --port RANDOM ... (use a random port) ║
║ ║
╚═════════════════════════════════════════════════════════════════════════════════╝
EOF
exit 1
fi
if [[ "$requested_port" == "$code_port" ]]; then
cat >&2 <<EOF
╔═════════════════════════════════════════════════════════════════════════════════╗
║ Port conflict in nested booth ║
╠═════════════════════════════════════════════════════════════════════════════════╣
║ The requested port ($requested_port) conflicts with this container's code port. ║
║ ║
║ Use a different port: ║
║ ./booth --port NEXT ... (auto-find next available port) ║
║ ./booth --port RANDOM ... (use a random port) ║
║ ║
╚═════════════════════════════════════════════════════════════════════════════════╝
EOF
exit 1
fi
fi
# All checks passed, allow nested execution
return 0
}
# --- CENTRAL CACHE SETUP ---
# Platform-specific cache directory for shared binary storage
get_cache_dir() {
case "$(uname -s)" in
Linux*)
echo "${XDG_CACHE_HOME:-$HOME/.cache}/codingbooth"
;;
Darwin*)
echo "$HOME/Library/Caches/codingbooth"
;;
MINGW*|MSYS*|CYGWIN*)
if [[ -n "${LOCALAPPDATA:-}" ]]; then
echo "$LOCALAPPDATA/codingbooth"
else
echo "$HOME/AppData/Local/codingbooth"
fi
;;
*)
echo "${XDG_CACHE_HOME:-$HOME/.cache}/codingbooth"
;;
esac
}
BOOTH_CACHE_DIR="${BOOTH_CACHE_DIR:-$(get_cache_dir)}"
# Get version-specific cache directory
get_cache_version_dir() {
local version="$1"
echo "${BOOTH_CACHE_DIR}/versions/${version}"
}
# Find binary directory: local first, then shared cache
# Returns directory path on stdout, returns 0 if found, 1 if not found
find_binary_dir() {
local version="$1"
local platform="$2"
local binary_name
binary_name=$(get_binary_name "$platform")
# Check local first (for --cache=local mode)
if [[ -f "${TOOLS_DIR}/${binary_name}" ]]; then
echo "$TOOLS_DIR"
return 0
fi
# Then check shared cache
local shared_dir
shared_dir="$(get_cache_version_dir "$version")"
if [[ -f "${shared_dir}/${binary_name}" ]]; then
echo "$shared_dir"
return 0
fi
# Not found
return 1
}
# Decide what the "command" is:
if [[ $# -eq 0 ]]; then COMMAND="run" ; else COMMAND="$1" ; fi
function Main() {
### --- COMMAND DISPATCH --- ###
case "${COMMAND}" in
uninstall) UninstallBooth ; exit 0 ; ;;
install)
# Parse --cache= flag
shift # Remove 'install'
local cache_mode="shared"
local version_arg=""
while [[ $# -gt 0 ]]; do
case "$1" in
--cache=*) cache_mode="${1#--cache=}"; shift ;;
--cache) cache_mode="$2"; shift 2 ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) version_arg="$1"; shift ;;
esac
done
# If no version specified and already installed, just print and exit
if [[ -z "$version_arg" && -f "$LOCK_FILE" ]]; then
local lock_version
lock_version=$(grep '^version=' "$LOCK_FILE" 2>/dev/null | cut -d= -f2-)
echo "CodingBooth is already installed (version: ${lock_version:-unknown})."
echo "Use '$0 update' to update to the latest version."
exit 0
fi
# Validate cache mode
if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then
echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2
exit 1
fi
DownloadBooth "${version_arg:-latest}" "$cache_mode"
exit 0
;;
update-wrapper)
shift # Remove 'update-wrapper'
local assume_yes="false"
while [[ $# -gt 0 ]]; do
case "$1" in
-y|--yes) assume_yes="true"; shift ;;
*) echo "Unknown option: $1" >&2
echo "Usage: $0 update-wrapper [--yes|-y]" >&2
exit 1 ;;
esac
done
UpdateWrapper "$assume_yes"
exit 0
;;
update)
# Parse --cache= flag
shift # Remove 'update'
local cache_mode="shared"
local version_arg="latest"
while [[ $# -gt 0 ]]; do
case "$1" in
--cache=*) cache_mode="${1#--cache=}"; shift ;;
--cache) cache_mode="$2"; shift 2 ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) version_arg="$1"; shift ;;
esac
done
# Validate cache mode
if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then
echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2
exit 1
fi
DownloadBooth "$version_arg" "$cache_mode"
exit 0
;;
tools-cache)
shift # Remove 'tools-cache'
case "${1:-list}" in
list) ToolsCacheList ;;
clean) shift; ToolsCacheClean "$@" ;;
*) echo "Unknown tools-cache command: $1" >&2; exit 1 ;;
esac
exit 0
;;
run) [[ "${1-}" == "run" ]] && shift ; ;;
*) ;;
esac
### --- RUN MODE --- ###
# Read version and cache mode from lock file
if [[ ! -f "$LOCK_FILE" ]]; then
echo "CodingBooth is not installed."
echo "Please run: $0 install"
exit 1
fi
local lock_version lock_cache
lock_version=$(grep '^version=' "$LOCK_FILE" 2>/dev/null | cut -d= -f2-)
lock_cache=$(grep '^cache=' "$LOCK_FILE" 2>/dev/null | cut -d= -f2- || echo "shared")
# Handle legacy lock files without cache= line
if [[ -z "$lock_cache" ]]; then
lock_cache="shared"
fi
if [[ -z "$lock_version" ]]; then
echo "Invalid lock file: missing version"
echo "Please run: $0 install"
exit 1
fi
# Detect platform
local platform binary_name
if ! platform=$(detect_platform); then
echo "Error: Failed to detect platform" >&2
exit 1
fi
binary_name=$(get_binary_name "$platform")
# Find binary directory (local first, then shared cache)
local binary_dir sha_file dest
if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then
# Binary not found, auto-download
echo "Binary missing, downloading version $lock_version..."
DownloadBooth "$lock_version" "$lock_cache"
if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then
echo "Failed to download binary"
exit 1
fi
fi
sha_file="$binary_dir/codingbooth.sha256"
dest="$binary_dir/$binary_name"
# Verify binary exists
if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then
echo "CodingBooth binary or checksum missing for platform: $platform"
echo "Please run: $0 install"
exit 1
fi
# Ensure binary is newer than checksum
if [[ "$dest" -ot "$sha_file" ]]; then
echo "Binary appears older than its checksum file."
echo "Run: $0 update to restore the official release."
exit 1
fi
# Verify SHA256 for this platform's binary
local expected_sha256 actual_sha256
expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}')
if [[ -z "$expected_sha256" ]]; then
echo "No SHA256 entry found for $binary_name"
echo "Run: $0 update to restore the official release."
exit 1
fi
actual_sha256=$(hash_sha256 "$dest" | awk '{gsub(/^\\/, ""); print $1}')
if [[ "$expected_sha256" != "$actual_sha256" ]]; then
echo "Binary ($binary_name) failed SHA256 verification."
echo "Run: $0 update to restore the official release."
exit 1
fi
# Create/update symlink named 'booth' so the binary displays correct name
local booth_link="$binary_dir/booth"
if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then
ln -sf "$binary_name" "$booth_link" 2>/dev/null || true
fi
# Execute via symlink if available, otherwise direct
if [[ -L "$booth_link" ]]; then
exec "$booth_link" "$@"
else
exec "$dest" "$@"
fi
}
function PrintHelp() {
cat <<EOF
Usage: ./$(basename "$0") <command> [args...]
Purpose:
This script is the *CodingBooth Wrapper*.
- It downloads, verifies, and runs the CodingBooth binary.
- Binaries are cached in a shared location (default) or per-project.
Wrapper commands:
install [VERSION] Install binary (skips if already installed)
install --cache=shared [VER] Install to shared cache (explicit)
install --cache=local [VER] Install to .booth/tools/ (project-local)
update [VERSION] Update binary to latest (or specified) version
update-wrapper [--yes|-y] Update the 'booth' wrapper script (latest only)
uninstall Remove project lock file and local binaries
tools-cache list Show cached binary versions and sizes
tools-cache clean Interactively remove cached versions
tools-cache clean --all Remove all cached versions
tools-cache clean VER Remove specific version
run [ARGS...] Run booth with ARGS (after integrity checks)
shell-config [--force] Add 'booth' command to your shell (bash/zsh)
version Show version information
help Show this help message
Cache modes:
--cache=shared Store in user cache, shared across projects (default)
--cache=local Store in .booth/tools/, project-specific
Cache locations (for --cache=shared):
Linux: ~/.cache/codingbooth/
macOS: ~/Library/Caches/codingbooth/
Windows: %LOCALAPPDATA%\\codingbooth\\
Notes:
- Lock file (.booth/tools/codingbooth.lock) is version-controlled
- Binary is auto-downloaded when lock file exists but binary missing
- Only the current platform's binary is downloaded
- Use --cache=local for CI/CD or portable/air-gapped environments
- Set VERBOSE=true for extra logs during install
Help/Version disambiguation:
./booth help Show this wrapper help (install, update, cache commands)
./booth --help Show codingbooth binary help (run flags, variants, etc.)
./booth version Show wrapper version + binary version info
./booth --version Show codingbooth binary version only
EOF
}
function ShellConfig__print_func() {
cat <<'EOF'
unalias booth 2>/dev/null
booth() {
local d="$PWD"
while :; do
if [[ -x "$d/booth" ]]; then
"$d/booth" "$@"
return $?
fi
[[ -z "$d" || "$d" == "/" ]] && break
d="$(dirname "$d")"
done
echo "CodingBooth wrapper not found in this directory tree." >&2
echo "What happened: booth was installed as a shell function, but no project booth script exists in the current path or parents." >&2
echo "" >&2
echo "More Information: " >&2
echo " https://github.com/NawaMan/CodingBooth/blob/latest/docs/troubleshoot/CBT0001-booth-wrapper-not-found.md" >&2
echo "" >&2
echo "Install booth in this project folder by running:" >&2
echo "> curl -fsSL https://github.com/NawaMan/CodingBooth/releases/download/latest/booth | bash" >&2
return 127
}
EOF
}
function ShellConfig() {
local force=false
while [[ $# -gt 0 ]]; do
case "$1" in
--force|-f) force=true; shift ;;
--eval|-e)
# Output raw function to stdout for eval "$(./booth shell-config --eval)"
ShellConfig__print_func
return 0
;;
--help|-h)
cat <<'EOF'
Usage: ./booth shell-config [--force] [--eval]
Options:
--force, -f Add/override booth() even if a custom booth() exists.
--eval, -e Print booth() function to stdout (for eval or sourcing).
EOF
return 0
;;
*)
echo "Unknown option for shell-config: $1" >&2
echo "Usage: ./booth shell-config [--force] [--eval]" >&2
return 1
;;
esac
done
local booth_info_url="https://github.com/NawaMan/CodingBooth/blob/latest/docs/troubleshoot/CBT0001-booth-wrapper-not-found.md"
local booth_func
booth_func="$(ShellConfig__print_func)"
# All supported shell rc files
local rc_files=(
"$HOME/.bashrc"
"$HOME/.zshrc"
"$HOME/.bash_profile"
"$HOME/.profile"
)
local added=()
local updated=()
local forced=()
local skipped=()
local custom_skipped=()
local not_found=()
for rc_file in "${rc_files[@]}"; do
if [[ ! -f "$rc_file" ]]; then
not_found+=("$rc_file")
continue
fi
# Already has the current function (idempotent)
if [[ "$force" != "true" ]] && grep -Fq "More Information: $booth_info_url" "$rc_file" 2>/dev/null; then
skipped+=("$rc_file")
continue
fi
# Upgrade older CodingBooth-managed function if present.
if grep -Eq '^# CodingBooth - run .+ from any subdirectory$' "$rc_file" 2>/dev/null; then
{
echo ""
echo "# CodingBooth - run 'booth' from any subdirectory"
echo "$booth_func"
} >> "$rc_file"
if [[ "$force" == "true" ]]; then
forced+=("$rc_file")
else
updated+=("$rc_file")
fi
continue
fi
# Avoid overriding custom user-defined booth() functions.
if grep -q 'booth()' "$rc_file" 2>/dev/null; then
if [[ "$force" == "true" ]]; then
{
echo ""
echo "# CodingBooth - run 'booth' from any subdirectory"
echo "$booth_func"
} >> "$rc_file"
forced+=("$rc_file")
else
custom_skipped+=("$rc_file")
fi
continue
fi
# Add the function
{
echo ""
echo "# CodingBooth - run 'booth' from any subdirectory"
echo "$booth_func"
} >> "$rc_file"
added+=("$rc_file")
done
# Report results
if [[ ${#added[@]} -gt 0 ]]; then
echo "✓ Added booth function to:"
for f in "${added[@]}"; do
echo " $f"
done
fi
if [[ ${#updated[@]} -gt 0 ]]; then
echo "✓ Updated booth function in:"
for f in "${updated[@]}"; do
echo " $f"
done
fi
if [[ ${#forced[@]} -gt 0 ]]; then
echo "✓ Force-installed booth function in:"
for f in "${forced[@]}"; do
echo " $f"
done
fi
if [[ ${#skipped[@]} -gt 0 ]]; then
echo "✓ Already configured (skipped):"
for f in "${skipped[@]}"; do
echo " $f"
done
fi
if [[ ${#custom_skipped[@]} -gt 0 ]]; then
echo "⚠ Custom booth() detected (not modified):"
for f in "${custom_skipped[@]}"; do
echo " $f"
done
fi
if [[ ${#added[@]} -eq 0 && ${#updated[@]} -eq 0 && ${#forced[@]} -eq 0 && ${#skipped[@]} -eq 0 && ${#custom_skipped[@]} -eq 0 ]]; then
echo "No shell config files found." >&2
echo "" >&2
echo "You can activate booth in this shell by running:" >&2
echo ' eval "$(./booth shell-config)"' >&2
echo "" >&2
# Output raw function code to stdout so eval "$(./booth shell-config)" works
echo "$booth_func"
exit 0
fi
echo ""
if [[ ${#added[@]} -gt 0 || ${#updated[@]} -gt 0 || ${#forced[@]} -gt 0 ]]; then
echo "To activate, restart your terminal or run:"
echo " source ~/.bashrc # for bash"
echo " source ~/.zshrc # for zsh"
echo ""
fi
echo "Then you can run 'booth' from any subdirectory of a CodingBooth project."
}
function PrintVersion() {
cat <<'EOF'
____ _ _ ____ _ _
/ ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__
| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \
| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | |
\____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_|
|___/
EOF
echo "CodingBooth Wrapper: $VERSION"
# Detect current platform
local platform binary_name
platform=$(detect_platform 2>/dev/null || echo "unknown")
binary_name=$(get_binary_name "$platform")
# Check if lock file exists
if [[ ! -f "$LOCK_FILE" ]]; then
echo "CodingBooth: not installed"
echo "Platform: $platform"
echo "Shared cache: $BOOTH_CACHE_DIR"
exit 0
fi
# Read lock file
local lock_version lock_cache
lock_version=$(grep '^version=' "$LOCK_FILE" 2>/dev/null | cut -d= -f2- || echo "unknown")
lock_cache=$(grep '^cache=' "$LOCK_FILE" 2>/dev/null | cut -d= -f2- || echo "shared")
# Find binary
local binary_dir TOOL
if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then
TOOL="$binary_dir/$binary_name"
else
echo "CodingBooth: $lock_version (binary missing)"
echo "Platform: $platform"
echo "Cache mode: $lock_cache"
exit 0
fi
[[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true
local TOOL_VERSION
TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown")
echo ""
echo "$TOOL_VERSION"
echo "Platform: $platform"
echo "Cache mode: $lock_cache"
if [[ "$lock_cache" == "shared" ]]; then
echo "Binary location: $binary_dir"
fi
}
# Portable SHA256 helper
function hash_sha256() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@"
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@"
else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1
fi
}
# Detect platform (OS-ARCH format)
function detect_platform() {
local os arch
# Detect OS
case "$(uname -s)" in
Linux*) os="linux" ;;
Darwin*) os="darwin" ;;
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
*) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;;
esac
# Detect architecture
case "$(uname -m)" in
x86_64|amd64) arch="amd64" ;;
aarch64|arm64) arch="arm64" ;;
*) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;;
esac
echo "${os}-${arch}"
}
# Get binary name for platform (adds .exe for Windows)
function get_binary_name() {
local platform="$1"
if [[ "$platform" == windows-* ]]; then
echo "codingbooth-${platform}.exe"
else
echo "codingbooth-${platform}"
fi
}
# All supported platforms (used by uninstall for cleanup)
ALL_PLATFORMS=(
"linux-amd64"
"linux-arm64"
"darwin-amd64"
"darwin-arm64"
"windows-amd64"
)
function UninstallBooth() {
local tools_dir="$TOOLS_DIR"
local sha_file="$tools_dir/codingbooth.sha256"
local lock_file="$LOCK_FILE"
# Remove local binaries if they exist
for platform in "${ALL_PLATFORMS[@]}"; do
local binary_name
binary_name=$(get_binary_name "$platform")
rm -f "$tools_dir/$binary_name"
done
rm -f "$sha_file" "$lock_file"
rmdir "$tools_dir" 2>/dev/null || true
rmdir "$BOOTH_DIR" 2>/dev/null || true
echo "CodingBooth has been uninstalled from this project."
echo "To clean shared cache, run: $0 tools-cache clean"
}
# Format bytes to human-readable size
format_size() {
local bytes=$1
if command -v numfmt >/dev/null 2>&1; then
numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B"
else
# Fallback for systems without numfmt (e.g., macOS)
if [[ $bytes -ge 1073741824 ]]; then
echo "$(( bytes / 1073741824 ))GiB"
elif [[ $bytes -ge 1048576 ]]; then
echo "$(( bytes / 1048576 ))MiB"
elif [[ $bytes -ge 1024 ]]; then
echo "$(( bytes / 1024 ))KiB"
else
echo "${bytes}B"
fi
fi
}
# Get directory size in bytes (portable)
get_dir_size() {
local dir="$1"
if [[ "$(uname -s)" == "Darwin" ]]; then
# macOS: du -sk gives size in KB
local kb
kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0)
echo $((kb * 1024))
else
# Linux: du -sb gives size in bytes
du -sb "$dir" 2>/dev/null | cut -f1 || echo 0
fi
}
function ToolsCacheList() {
local versions_dir="${BOOTH_CACHE_DIR}/versions"
echo ""
if [[ ! -d "$versions_dir" ]]; then
echo "No cached versions found."
echo ""
echo "Cache location: $BOOTH_CACHE_DIR"
return 0
fi
echo "Cached binary versions:"
echo ""
local total_size=0
local version_count=0
for version_dir in "$versions_dir"/*/; do
[[ ! -d "$version_dir" ]] && continue
local version
version=$(basename "$version_dir")
# Calculate size
local size_bytes size_human
size_bytes=$(get_dir_size "$version_dir")
size_human=$(format_size "$size_bytes")
# List platforms
local platforms=()
for bin in "$version_dir"/codingbooth-*; do
[[ -f "$bin" ]] || continue
local name
name=$(basename "$bin")
name=${name#codingbooth-}
name=${name%.exe}
platforms+=("$name")
done
printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}"
total_size=$((total_size + size_bytes))
: $((version_count++))
done
if [[ $version_count -eq 0 ]]; then
echo " (no versions cached)"
fi
echo ""
local total_human
total_human=$(format_size "$total_size")
echo "Total: $total_human in $version_count version(s)"
echo ""
echo "Cache location: $BOOTH_CACHE_DIR"
}
function ToolsCacheClean() {
local versions_dir="${BOOTH_CACHE_DIR}/versions"
if [[ ! -d "$versions_dir" ]]; then
echo "No cached versions to clean."
return 0
fi
# Parse arguments
local clean_all=false
local target_version=""
while [[ $# -gt 0 ]]; do
case "$1" in
--all) clean_all=true; shift ;;
--unused) echo "Warning: --unused not yet implemented"; shift ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) target_version="$1"; shift ;;
esac
done
if [[ "$clean_all" == "true" ]]; then
local size_before
size_before=$(get_dir_size "$versions_dir")
rm -rf "$versions_dir"
local size_human
size_human=$(format_size "$size_before")
echo "Removed all cached versions. Freed $size_human"
return 0
fi
if [[ -n "$target_version" ]]; then
local target_dir="$versions_dir/$target_version"
if [[ ! -d "$target_dir" ]]; then
echo "Version $target_version not found in cache."
return 1
fi
local size_before
size_before=$(get_dir_size "$target_dir")
rm -rf "$target_dir"
local size_human
size_human=$(format_size "$size_before")
echo "Removed version $target_version. Freed $size_human"
return 0
fi
# Interactive mode: list and prompt
ToolsCacheList
echo ""
read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice
if [[ "$choice" == "all" ]]; then
ToolsCacheClean --all
elif [[ -n "$choice" ]]; then
ToolsCacheClean "$choice"
else
echo "No version selected."
fi
}
function UpdateWrapper() {
local assume_yes="${1:-false}"
# Resolve the on-disk path of this wrapper script (follows symlinks).
local source="${BASH_SOURCE[0]}"
while [[ -L "$source" ]]; do
local link_dir
link_dir="$(cd -P "$(dirname "$source")" && pwd)"
source="$(readlink "$source")"
[[ "$source" != /* ]] && source="$link_dir/$source"
done
local target_dir target
target_dir="$(cd -P "$(dirname "$source")" && pwd)"
target="$target_dir/$(basename "$source")"
if [[ ! -f "$target" ]]; then
echo "Error: cannot locate wrapper script at $target" >&2
exit 1
fi
if [[ ! -w "$target" || ! -w "$target_dir" ]]; then
echo "Error: $target_dir is not writable" >&2
exit 1
fi
echo "This will replace the wrapper at:"
echo " $target"
echo "with the latest version, by running:"
echo " curl -fsSL https://codingbooth.io/install.sh | bash"
echo ""
echo "Current wrapper version: $VERSION"
echo ""
if [[ "$assume_yes" != "true" ]]; then
if [[ ! -t 0 ]]; then
echo "Error: not connected to a terminal — re-run with --yes to skip the prompt." >&2
exit 1
fi
local reply
read -rp "Proceed? [y/N] " reply
case "$reply" in
y|Y|yes|YES|Yes) ;;
*) echo "Cancelled." ; exit 0 ;;
esac
echo ""
fi
# Run the canonical install command from the wrapper's directory so the new
# wrapper lands in the right place.
( cd "$target_dir" && curl -fsSL https://codingbooth.io/install.sh | bash )
}
function DownloadBooth() {
local CB_VERSION=${1:-latest}
local CACHE_MODE=${2:-shared}
# Detect common mistake: using --version flag instead of positional argument
if [[ "$CB_VERSION" == --* ]]; then
echo "Error: Invalid version '$CB_VERSION'" >&2
echo "Usage: $0 install [VERSION]" >&2