|
#!/bin/bash |
|
set -e |
|
|
|
# Error message for non-interactive environments |
|
terminal_error() { |
|
_ip=$(hostname -I 2>/dev/null | awk '{print $1}') |
|
_host="${_ip:-<your-unraid-ip>}" |
|
echo "" |
|
echo " Error: This installer requires an interactive terminal." |
|
echo " The Unraid web terminal does not support interactive input." |
|
echo "" |
|
echo " Use SSH instead:" |
|
echo " ssh root@${_host}" |
|
echo "" |
|
exit 1 |
|
} |
|
|
|
# Detect web terminal (ttyd/noVNC/shellinabox) — /dev/tty exists but read hangs |
|
if [ -z "$SSH_TTY" ] && [ -z "$SSH_CONNECTION" ]; then |
|
if pstree -s $$ 2>/dev/null | grep -qiE 'ttyd|shellinabox|novnc|websockify'; then |
|
terminal_error |
|
elif ! tty 2>/dev/null | grep -qE '/dev/(tty[0-9]|pts/)'; then |
|
terminal_error |
|
fi |
|
fi |
|
|
|
# Set up interactive input: |
|
# - stdin is a terminal (bash install.sh) → use stdin directly |
|
# - stdin is a pipe (curl | bash) → fall back to /dev/tty |
|
if [ -t 0 ]; then |
|
INPUT_FD=0 |
|
prompt() { |
|
read -p "$1" "$2" |
|
} |
|
readchar() { |
|
IFS= read -r -n 1 _ch 2>/dev/null |
|
printf '%s' "$_ch" |
|
} |
|
else |
|
exec 3</dev/tty 2>/dev/null || terminal_error |
|
INPUT_FD=3 |
|
prompt() { |
|
read -p "$1" "$2" <&3 |
|
} |
|
readchar() { |
|
IFS= read -r -n 1 _ch <&3 2>/dev/null |
|
printf '%s' "$_ch" |
|
} |
|
fi |
|
|
|
echo "" |
|
echo " Claude Code on Unraid — Installer" |
|
echo " ==================================" |
|
echo "" |
|
|
|
# --- Detect storage pool --- |
|
# Unraid 6.12+ allows named pools (cache, nvme, ssd, fast, ...). |
|
# Find the pool that holds appdata, or fall back to first available. |
|
|
|
POOL="" |
|
# 1. Check for existing claude-code appdata |
|
for p in /mnt/*/appdata/claude-code; do |
|
[ -d "$p" ] && POOL=$(echo "$p" | cut -d/ -f3) && break |
|
done |
|
# 2. Check for any appdata directory |
|
if [ -z "$POOL" ]; then |
|
for p in /mnt/*/appdata; do |
|
[ -d "$p" ] && POOL=$(echo "$p" | cut -d/ -f3) && break |
|
done |
|
fi |
|
# 3. Fall back to common pool names |
|
if [ -z "$POOL" ]; then |
|
for name in cache nvme ssd fast; do |
|
[ -d "/mnt/$name" ] && POOL="$name" && break |
|
done |
|
fi |
|
# 4. Last resort: first non-special pool |
|
if [ -z "$POOL" ]; then |
|
for p in /mnt/*/; do |
|
[ -d "$p" ] || continue |
|
pname=$(basename "$p") |
|
case "$pname" in user|user0|disk[0-9]*|disks|rootshare|addons|remotes) continue ;; esac |
|
POOL="$pname" |
|
break |
|
done |
|
fi |
|
|
|
if [ -z "$POOL" ]; then |
|
echo "Error: No storage pool found. Is the array started?" |
|
exit 1 |
|
fi |
|
|
|
# --- Prerequisites --- |
|
|
|
if ! command -v docker &>/dev/null; then |
|
echo "Error: Docker is not available. Is the Docker service running?" |
|
exit 1 |
|
fi |
|
|
|
if docker compose version &>/dev/null 2>&1; then |
|
COMPOSE="docker compose" |
|
elif command -v docker-compose &>/dev/null; then |
|
COMPOSE="docker-compose" |
|
else |
|
echo "Error: Docker Compose is not available." |
|
echo "Install the Compose Manager plugin from Community Applications." |
|
exit 1 |
|
fi |
|
|
|
# --- Handle existing container --- |
|
|
|
if docker ps -a --format '{{.Names}}' | grep -q '^claude-code$'; then |
|
echo "An existing claude-code container was found." |
|
prompt "Stop and remove it to continue? [Y/n]: " remove_existing |
|
if [[ "$remove_existing" =~ ^[nN] ]]; then |
|
echo "Aborted." |
|
exit 0 |
|
fi |
|
docker stop claude-code 2>/dev/null || true |
|
docker rm claude-code 2>/dev/null || true |
|
echo "" |
|
fi |
|
|
|
# --- Workspace & Home --- |
|
|
|
WS_DEFAULT="/mnt/${POOL}/appdata/claude-code/workspace" |
|
prompt "Workspace directory [${WS_DEFAULT}]: " WORKSPACE |
|
WORKSPACE="${WORKSPACE:-${WS_DEFAULT}}" |
|
|
|
HOME_DEFAULT="/mnt/${POOL}/appdata/claude-code/home" |
|
prompt "Home directory (Claude config & state) [${HOME_DEFAULT}]: " CLAUDE_HOME |
|
CLAUDE_HOME="${CLAUDE_HOME:-${HOME_DEFAULT}}" |
|
|
|
# --- Migrate old config --- |
|
|
|
OLD_APPDATA="/mnt/${POOL}/appdata/claude-code" |
|
if [ -f "${OLD_APPDATA}/settings.json" ] || \ |
|
[ -d "${OLD_APPDATA}/projects" ] || \ |
|
[ -d "${OLD_APPDATA}/credentials" ]; then |
|
echo "Found existing Claude config from a previous setup." |
|
echo "The new setup stores everything under ${CLAUDE_HOME}/.claude/" |
|
prompt "Move existing config there now? [Y/n]: " migrate |
|
if [[ ! "$migrate" =~ ^[nN] ]]; then |
|
mkdir -p ${CLAUDE_HOME}/.claude |
|
shopt -s dotglob |
|
for item in ${OLD_APPDATA}/*; do |
|
name=$(basename "$item") |
|
case "$name" in |
|
home|Dockerfile|install.sh|docker-compose.yml|*.md) continue ;; |
|
esac |
|
mv "$item" ${CLAUDE_HOME}/.claude/ 2>/dev/null || true |
|
done |
|
shopt -u dotglob |
|
echo "Done." |
|
fi |
|
echo "" |
|
fi |
|
|
|
# --- Volume Selection --- |
|
|
|
# Build volume list |
|
VOLUMES=() |
|
VOLUMES+=("${CLAUDE_HOME}:/root:rw") |
|
VOLUMES+=("${WORKSPACE}:/workspace:rw") |
|
|
|
# Collect all mountable items: name, source path, category, default state |
|
ITEMS_NAME=() |
|
ITEMS_PATH=() |
|
ITEMS_CAT=() |
|
ITEMS_STATE=() # skip, ro, rw |
|
|
|
# Data shares from /mnt/user/ |
|
if [ -d "/mnt/user" ]; then |
|
for dir in /mnt/user/*/; do |
|
[ -d "$dir" ] || continue |
|
name=$(basename "$dir") |
|
case "$name" in claude-code) continue ;; esac |
|
case "$name" in |
|
appdata|system|domains|docker|isos) ;; # handled below as system |
|
*) |
|
ITEMS_NAME+=("$name") |
|
ITEMS_PATH+=("/mnt/user/${name}") |
|
ITEMS_CAT+=("data") |
|
ITEMS_STATE+=("skip") |
|
;; |
|
esac |
|
done |
|
fi |
|
|
|
# System shares from /mnt/user/ |
|
for name in appdata system domains docker isos; do |
|
[ -d "/mnt/user/$name" ] || continue |
|
ITEMS_NAME+=("$name") |
|
ITEMS_PATH+=("/mnt/user/${name}") |
|
ITEMS_CAT+=("system") |
|
ITEMS_STATE+=("skip") |
|
done |
|
|
|
# System paths |
|
if [ -d "/boot" ]; then |
|
ITEMS_NAME+=("/boot") |
|
ITEMS_PATH+=("/boot") |
|
ITEMS_CAT+=("paths") |
|
ITEMS_STATE+=("skip") |
|
fi |
|
if [ -d "/var/log" ]; then |
|
ITEMS_NAME+=("/var/log") |
|
ITEMS_PATH+=("/var/log") |
|
ITEMS_CAT+=("paths") |
|
ITEMS_STATE+=("skip") |
|
fi |
|
|
|
# Unassigned devices |
|
if [ -d "/mnt/disks" ]; then |
|
for dir in /mnt/disks/*/; do |
|
[ -d "$dir" ] || continue |
|
name=$(basename "$dir") |
|
ITEMS_NAME+=("$name") |
|
ITEMS_PATH+=("/mnt/disks/${name}") |
|
ITEMS_CAT+=("disks") |
|
ITEMS_STATE+=("skip") |
|
done |
|
fi |
|
|
|
# Remote mounts |
|
if [ -d "/mnt/remotes" ]; then |
|
for dir in /mnt/remotes/*/; do |
|
[ -d "$dir" ] || continue |
|
name=$(basename "$dir") |
|
ITEMS_NAME+=("$name") |
|
ITEMS_PATH+=("/mnt/remotes/${name}") |
|
ITEMS_CAT+=("remotes") |
|
ITEMS_STATE+=("skip") |
|
done |
|
fi |
|
|
|
# Docker socket (special toggle) |
|
DOCKER_ACCESS="yes" |
|
|
|
TOTAL=${#ITEMS_NAME[@]} |
|
|
|
# Disable set -e for TUI — dd/read/stty can return non-zero harmlessly |
|
set +e |
|
|
|
if [ "$TOTAL" -eq 0 ]; then |
|
echo "" |
|
echo " No shares found. You can add volumes manually later." |
|
else |
|
# --- Interactive toggle selector --- |
|
# Pure bash TUI: arrow keys navigate, space cycles skip→ro→rw, enter confirms |
|
|
|
# Build display lines with category headers |
|
# Map: display line index → item index (or -1 for header/blank) |
|
DISPLAY_LINES=() |
|
DISPLAY_TEXT=() |
|
LINE_TO_ITEM=() |
|
|
|
add_category() { |
|
local cat="$1" header="$2" |
|
local found=false |
|
for i in "${!ITEMS_CAT[@]}"; do |
|
[ "${ITEMS_CAT[$i]}" = "$cat" ] && found=true && break |
|
done |
|
$found || return |
|
|
|
# Blank line before header (except first) |
|
if [ ${#DISPLAY_LINES[@]} -gt 0 ]; then |
|
DISPLAY_LINES+=("") |
|
DISPLAY_TEXT+=("") |
|
LINE_TO_ITEM+=(-1) |
|
fi |
|
|
|
DISPLAY_LINES+=("header") |
|
DISPLAY_TEXT+=("$header") |
|
LINE_TO_ITEM+=(-1) |
|
|
|
for i in "${!ITEMS_CAT[@]}"; do |
|
if [ "${ITEMS_CAT[$i]}" = "$cat" ]; then |
|
DISPLAY_LINES+=("item") |
|
DISPLAY_TEXT+=("${ITEMS_NAME[$i]}") |
|
LINE_TO_ITEM+=("$i") |
|
fi |
|
done |
|
} |
|
|
|
add_category "data" " Data shares:" |
|
add_category "system" " System shares (Docker/VM config, system data):" |
|
add_category "paths" " System paths:" |
|
add_category "disks" " Unassigned devices:" |
|
add_category "remotes" " Remote mounts:" |
|
|
|
# Add docker socket toggle |
|
DISPLAY_LINES+=("") |
|
DISPLAY_TEXT+=("") |
|
LINE_TO_ITEM+=(-1) |
|
DISPLAY_LINES+=("header") |
|
DISPLAY_TEXT+=(" Other:") |
|
LINE_TO_ITEM+=(-1) |
|
DISPLAY_LINES+=("docker") |
|
DISPLAY_TEXT+=("Docker socket") |
|
LINE_TO_ITEM+=(-2) |
|
|
|
NUM_LINES=${#DISPLAY_LINES[@]} |
|
|
|
# Find first selectable line |
|
cursor=0 |
|
for i in "${!DISPLAY_LINES[@]}"; do |
|
if [ "${DISPLAY_LINES[$i]}" = "item" ] || [ "${DISPLAY_LINES[$i]}" = "docker" ]; then |
|
cursor=$i |
|
break |
|
fi |
|
done |
|
|
|
state_label() { |
|
case "$1" in |
|
skip) printf " -- " ;; |
|
ro) printf " ro " ;; |
|
rw) printf " RW " ;; |
|
esac |
|
} |
|
|
|
draw_menu() { |
|
# Move cursor to top of menu area and redraw |
|
if [ "${1:-}" = "redraw" ]; then |
|
printf "\033[%dA" "$NUM_LINES" |
|
fi |
|
|
|
_i=0 |
|
while [ "$_i" -lt "$NUM_LINES" ]; do |
|
_type="${DISPLAY_LINES[$_i]}" |
|
_text="${DISPLAY_TEXT[$_i]}" |
|
_idx="${LINE_TO_ITEM[$_i]}" |
|
|
|
printf "\033[2K" # clear line |
|
|
|
if [ "$_type" = "header" ]; then |
|
printf "\033[1m%s\033[0m\n" "$_text" |
|
elif [ "$_type" = "item" ]; then |
|
_st="${ITEMS_STATE[$_idx]}" |
|
if [ "$_i" -eq "$cursor" ]; then |
|
printf " \033[7m[%s]\033[0m %s\n" "$(state_label "$_st")" "$_text" |
|
else |
|
printf " [%s] %s\n" "$(state_label "$_st")" "$_text" |
|
fi |
|
elif [ "$_type" = "docker" ]; then |
|
if [ "$DOCKER_ACCESS" = "yes" ]; then _dl=" RW "; else _dl=" -- "; fi |
|
if [ "$_i" -eq "$cursor" ]; then |
|
printf " \033[7m[%s]\033[0m %s (/var/run/docker.sock)\n" "$_dl" "$_text" |
|
else |
|
printf " [%s] %s (/var/run/docker.sock)\n" "$_dl" "$_text" |
|
fi |
|
else |
|
printf "\n" |
|
fi |
|
_i=$((_i + 1)) |
|
done |
|
} |
|
|
|
# Save terminal state and enable raw mode |
|
old_stty=$(stty -g) |
|
|
|
echo "" |
|
echo " Select volumes for Claude Code" |
|
echo " Use arrow keys to navigate, Space to toggle (skip/ro/rw), Enter to confirm" |
|
echo "" |
|
|
|
draw_menu "first" |
|
|
|
# Ensure terminal is restored on exit/error |
|
trap 'stty "$old_stty" 2>/dev/null' EXIT |
|
|
|
if [ "$INPUT_FD" -eq 3 ]; then |
|
stty -echo -icanon min 1 <&3 |
|
else |
|
stty -echo -icanon min 1 |
|
fi |
|
|
|
while true; do |
|
char=$(readchar) |
|
|
|
if [ "$char" = $'\033' ]; then |
|
# Escape sequence — read next two chars for arrow keys |
|
char2=$(readchar) |
|
char3=$(readchar) |
|
if [ "$char2" = "[" ]; then |
|
case "$char3" in |
|
A) # Up arrow |
|
_nav=$((cursor - 1)) |
|
while [ "$_nav" -ge 0 ]; do |
|
if [ "${DISPLAY_LINES[$_nav]}" = "item" ] || [ "${DISPLAY_LINES[$_nav]}" = "docker" ]; then |
|
cursor=$_nav; break |
|
fi |
|
_nav=$((_nav - 1)) |
|
done |
|
;; |
|
B) # Down arrow |
|
_nav=$((cursor + 1)) |
|
while [ "$_nav" -lt "$NUM_LINES" ]; do |
|
if [ "${DISPLAY_LINES[$_nav]}" = "item" ] || [ "${DISPLAY_LINES[$_nav]}" = "docker" ]; then |
|
cursor=$_nav; break |
|
fi |
|
_nav=$((_nav + 1)) |
|
done |
|
;; |
|
esac |
|
fi |
|
draw_menu "redraw" |
|
|
|
elif [ "$char" = " " ]; then |
|
# Space — cycle state |
|
_sel="${LINE_TO_ITEM[$cursor]}" |
|
if [ "$_sel" -eq -2 ]; then |
|
# Docker toggle (on/off) |
|
if [ "$DOCKER_ACCESS" = "yes" ]; then DOCKER_ACCESS="no"; else DOCKER_ACCESS="yes"; fi |
|
elif [ "$_sel" -ge 0 ]; then |
|
case "${ITEMS_STATE[$_sel]}" in |
|
skip) ITEMS_STATE[$_sel]="ro" ;; |
|
ro) ITEMS_STATE[$_sel]="rw" ;; |
|
rw) ITEMS_STATE[$_sel]="skip" ;; |
|
esac |
|
fi |
|
draw_menu "redraw" |
|
|
|
elif [ "$char" = "" ] || [ "$char" = $'\n' ]; then |
|
# Enter — confirm selection |
|
break |
|
fi |
|
done |
|
|
|
# Restore terminal |
|
stty "$old_stty" |
|
trap - EXIT |
|
|
|
# Apply selections to VOLUMES + build summary |
|
USED_TARGETS=() |
|
_summary="" |
|
for _i in "${!ITEMS_NAME[@]}"; do |
|
_st="${ITEMS_STATE[$_i]}" |
|
[ "$_st" = "skip" ] && continue |
|
|
|
_src="${ITEMS_PATH[$_i]}" |
|
_name="${ITEMS_NAME[$_i]}" |
|
_cat="${ITEMS_CAT[$_i]}" |
|
case "$_cat" in |
|
paths) _target="/host${_name}" ;; # /boot → /host/boot, /var/log → /host/var/log |
|
*) _target="/${_name}" ;; |
|
esac |
|
|
|
# Collision check: ensure target isn't already used |
|
_base_target="$_target" |
|
if printf '%s\n' "${USED_TARGETS[@]}" | grep -qx "$_target" 2>/dev/null; then |
|
_target="/$(echo "${_cat}-${_name}" | tr '/' '-' | sed 's/^-//')" |
|
fi |
|
USED_TARGETS+=("$_target") |
|
|
|
VOLUMES+=("${_src}:${_target}:${_st}") |
|
_summary="${_summary} ${_name} (${_st})\n" |
|
done |
|
|
|
# Docker socket |
|
if [ "$DOCKER_ACCESS" = "yes" ]; then |
|
VOLUMES+=("/var/run/docker.sock:/var/run/docker.sock:rw") |
|
_summary="${_summary} Docker socket (rw)\n" |
|
fi |
|
|
|
# Show confirmation |
|
echo "" |
|
if [ -n "$_summary" ]; then |
|
echo " Selected:" |
|
printf "$_summary" |
|
else |
|
echo " No volumes selected." |
|
fi |
|
fi |
|
|
|
# Docker socket (if no TUI was shown) |
|
if [ "$TOTAL" -eq 0 ] && [ "$DOCKER_ACCESS" = "yes" ]; then |
|
VOLUMES+=("/var/run/docker.sock:/var/run/docker.sock:rw") |
|
fi |
|
|
|
# Re-enable strict mode |
|
set -e |
|
|
|
# --- tmux configuration --- |
|
|
|
mkdir -p ${CLAUDE_HOME} |
|
TMUX_CONF="${CLAUDE_HOME}/.tmux.conf" |
|
if [ -f "$TMUX_CONF" ]; then |
|
echo "Existing .tmux.conf found, keeping it." |
|
else |
|
cat > "$TMUX_CONF" << 'TMUXEOF' |
|
set -g history-limit 50000 |
|
set -g default-terminal "tmux-256color" |
|
set -g base-index 1 |
|
setw -g pane-base-index 1 |
|
|
|
# Mouse on for scroll wheel, but drag passes through to terminal for native copy |
|
set -g mouse on |
|
|
|
# Unbind drag — lets terminal handle text selection natively |
|
unbind -n MouseDrag1Pane |
|
unbind -T copy-mode-vi MouseDrag1Pane |
|
unbind -T copy-mode-vi MouseDragEnd1Pane |
|
unbind -T copy-mode MouseDrag1Pane |
|
unbind -T copy-mode MouseDragEnd1Pane |
|
|
|
# Scroll wheel enters copy mode and scrolls tmux history |
|
bind -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'select-pane -t=; copy-mode -e; send-keys -M'" |
|
bind -n WheelDownPane select-pane -t= \; send-keys -M |
|
|
|
# Ctrl+B, m to toggle full mouse mode on/off (for pane resize etc.) |
|
bind m set -g mouse \; display "Mouse: #{?mouse,ON,OFF}" |
|
TMUXEOF |
|
fi |
|
|
|
# --- Build compose volume block --- |
|
|
|
VOLUME_BLOCK="" |
|
for v in "${VOLUMES[@]}"; do |
|
VOLUME_BLOCK="${VOLUME_BLOCK} - ${v}"$'\n' |
|
done |
|
|
|
# --- Create files --- |
|
|
|
echo "" |
|
echo "Creating files..." |
|
|
|
mkdir -p /boot/config/plugins/compose.manager/projects/claude-code |
|
mkdir -p "$WORKSPACE" |
|
mkdir -p "$CLAUDE_HOME" |
|
|
|
# Prevent home/ from being sent as build context |
|
cat > ${WORKSPACE}/.dockerignore << 'EOF' |
|
* |
|
!Dockerfile |
|
EOF |
|
|
|
cat > ${WORKSPACE}/Dockerfile << 'EOF' |
|
FROM node:20-bookworm |
|
|
|
RUN apt-get update && \ |
|
apt-get install -y --no-install-recommends \ |
|
tmux git curl openssh-client docker.io ripgrep jq htop nano locales && \ |
|
apt-get clean && \ |
|
rm -rf /var/lib/apt/lists/* && \ |
|
sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \ |
|
locale-gen |
|
|
|
RUN npm install -g @anthropic-ai/claude-code |
|
|
|
ENV LANG=en_US.UTF-8 |
|
ENV LC_ALL=en_US.UTF-8 |
|
|
|
CMD ["tail", "-f", "/dev/null"] |
|
EOF |
|
|
|
cat > /boot/config/plugins/compose.manager/projects/claude-code/docker-compose.yml << COMPOSE |
|
services: |
|
claude-code: |
|
build: |
|
context: ${WORKSPACE} |
|
dockerfile: Dockerfile |
|
container_name: claude-code |
|
restart: unless-stopped |
|
hostname: unraid-claude |
|
network_mode: host |
|
privileged: true |
|
volumes: |
|
${VOLUME_BLOCK} working_dir: /workspace |
|
user: root |
|
environment: |
|
- NODE_ENV=development |
|
- SHELL=/bin/bash |
|
- TERM=xterm-256color |
|
- LANG=en_US.UTF-8 |
|
- LC_ALL=en_US.UTF-8 |
|
labels: |
|
- "com.unraid.docker.managed=composeman" |
|
- "com.unraid.docker.icon=https://cdn.anthropic.com/images/icons/claude.svg" |
|
COMPOSE |
|
|
|
# --- Host shortcut --- |
|
|
|
cat > /usr/local/bin/claude << 'SCRIPT' |
|
#!/bin/bash |
|
if ! docker ps --format '{{.Names}}' | grep -q '^claude-code$'; then |
|
if docker ps -a --format '{{.Names}}' | grep -q '^claude-code$'; then |
|
echo "Starting claude-code container..." |
|
docker start claude-code >/dev/null |
|
sleep 2 |
|
else |
|
echo "claude-code container not found. Run the installer first." |
|
exit 1 |
|
fi |
|
fi |
|
docker exec -it claude-code bash -c ' |
|
tmux has-session -t cc 2>/dev/null && tmux attach -t cc || tmux new -s cc "claude" |
|
' |
|
SCRIPT |
|
chmod +x /usr/local/bin/claude |
|
|
|
# Persist shortcut across reboots |
|
if ! grep -q "Claude Code shortcut" /boot/config/go 2>/dev/null; then |
|
cat >> /boot/config/go << 'BOOT' |
|
|
|
# Claude Code shortcut |
|
cat > /usr/local/bin/claude << 'SHORTCUT' |
|
#!/bin/bash |
|
if ! docker ps --format '{{.Names}}' | grep -q '^claude-code$'; then |
|
if docker ps -a --format '{{.Names}}' | grep -q '^claude-code$'; then |
|
echo "Starting claude-code container..." |
|
docker start claude-code >/dev/null |
|
sleep 2 |
|
else |
|
echo "claude-code container not found. Run the installer first." |
|
exit 1 |
|
fi |
|
fi |
|
docker exec -it claude-code bash -c ' |
|
tmux has-session -t cc 2>/dev/null && tmux attach -t cc || tmux new -s cc "claude" |
|
' |
|
SHORTCUT |
|
chmod +x /usr/local/bin/claude |
|
BOOT |
|
fi |
|
|
|
# --- Summary --- |
|
|
|
echo "" |
|
echo "=== Configuration ===" |
|
echo "" |
|
echo "Home: ${CLAUDE_HOME}" |
|
echo "Workspace: ${WORKSPACE}" |
|
echo "" |
|
echo "Volumes:" |
|
for v in "${VOLUMES[@]}"; do |
|
echo " - $v" |
|
done |
|
echo "" |
|
echo "Files:" |
|
echo " Dockerfile: ${WORKSPACE}/Dockerfile" |
|
echo " Compose: /boot/config/plugins/compose.manager/projects/claude-code/docker-compose.yml" |
|
echo "" |
|
prompt "Build and start now? [Y/n]: " confirm |
|
if [[ "$confirm" =~ ^[nN] ]]; then |
|
echo "" |
|
echo "When ready:" |
|
echo " cd /boot/config/plugins/compose.manager/projects/claude-code" |
|
echo " $COMPOSE build && $COMPOSE up -d" |
|
exit 0 |
|
fi |
|
|
|
echo "" |
|
echo "Building image (takes ~2 min on first run)..." |
|
cd /boot/config/plugins/compose.manager/projects/claude-code |
|
$COMPOSE build |
|
$COMPOSE up -d |
|
|
|
echo "" |
|
echo "=== Done! ===" |
|
echo "" |
|
echo "Type 'claude' to start Claude Code." |
|
echo "" |
|
echo " Detach session: Ctrl+B, D" |
|
echo " Reattach: claude" |
|
echo " Copy: select text normally, Ctrl+Shift+C" |
|
echo " Paste: Ctrl+Shift+V" |
|
echo " Scroll: mouse wheel" |
|
|
|
# Close fd 3 if it was opened |
|
exec 3<&- 2>/dev/null || true |