240 lines
7.8 KiB
Bash
Executable File
240 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
|
REPO_DIR="$(pwd)"
|
|
|
|
# Run `./claudaris config` to set your own image/container name (so multiple
|
|
# people can each run their own container from this repo) and other options.
|
|
# It writes them to .env, which is gitignored and sourced here if present.
|
|
NAME=claudaris
|
|
CLAUDARIS_IMAGE=4lights/claudaris:latest
|
|
[ -f .env ] && set -a && . .env && set +a
|
|
|
|
# docker-compose.yml reads NAME/DATA_DIR/WORKSPACE_DIR for variable
|
|
# substitution, so every subcommand that shells out to `docker compose`
|
|
# needs them exported, with the same defaults `cmd_start` seeds on disk.
|
|
export NAME
|
|
export CLAUDARIS_IMAGE
|
|
export DATA_DIR="${DATA_DIR:-/data/$NAME}"
|
|
export WORKSPACE_DIR="${WORKSPACE_DIR:-/home/${USER:-$(id -un)}/projects}"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: ./claudaris <command>
|
|
|
|
Commands:
|
|
config, configure Interactive wizard to write .env with your settings
|
|
build Build the container image (always --no-cache --pull)
|
|
start Start (or create) the container
|
|
connect Start the container if needed, then attach a bash shell
|
|
remove, stop, rm Stop and remove the container
|
|
help Show this message
|
|
EOF
|
|
}
|
|
|
|
# Ensure a path exists as a directory.
|
|
#
|
|
# This deliberately fails if the path already exists as a file. We don't want
|
|
# to silently replace a file with a directory because Docker bind mounts are
|
|
# sensitive to source/target types.
|
|
ensure_dir() {
|
|
local path="$1"
|
|
|
|
if [ -e "$path" ] && [ ! -d "$path" ]; then
|
|
echo "ERROR: expected directory, but found a non-directory:" >&2
|
|
echo " $path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$path"
|
|
}
|
|
|
|
# Ensure a path exists as a regular file.
|
|
#
|
|
# This deliberately fails if the path already exists as a directory. Docker
|
|
# will otherwise produce the confusing:
|
|
#
|
|
# not a directory: Are you trying to mount a directory onto a file
|
|
#
|
|
ensure_file() {
|
|
local path="$1"
|
|
|
|
if [ -e "$path" ] && [ ! -f "$path" ]; then
|
|
echo "ERROR: expected regular file, but found a directory or other type:" >&2
|
|
echo " $path" >&2
|
|
echo >&2
|
|
echo "Remove or rename the incorrect path, then run ./claudaris start again." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -e "$path" ]; then
|
|
touch "$path"
|
|
fi
|
|
}
|
|
|
|
cmd_config() {
|
|
echo "claudaris config — press enter to keep the current/default value."
|
|
echo
|
|
|
|
read -r -p "NAME [$NAME]: " input
|
|
NAME="${input:-$NAME}"
|
|
|
|
local data_dir_default="${DATA_DIR:-/data/$NAME}"
|
|
read -r -p "DATA_DIR [$data_dir_default]: " input
|
|
DATA_DIR="${input:-$data_dir_default}"
|
|
|
|
local workspace_dir_default="${WORKSPACE_DIR:-/home/${USER:-$(id -un)}/projects}"
|
|
read -r -p "WORKSPACE_DIR [$workspace_dir_default]: " input
|
|
WORKSPACE_DIR="${input:-$workspace_dir_default}"
|
|
|
|
local claudaris_image_default="${CLAUDARIS_IMAGE:-4lights/claudaris:latest}"
|
|
read -r -p "CLAUDARIS_IMAGE [$claudaris_image_default]: " input
|
|
CLAUDARIS_IMAGE="${input:-$claudaris_image_default}"
|
|
|
|
cat > .env <<EOF
|
|
# Written by \`./claudaris config\` — gitignored, per-user.
|
|
NAME=$NAME
|
|
CLAUDARIS_IMAGE=$CLAUDARIS_IMAGE
|
|
DATA_DIR=$DATA_DIR
|
|
WORKSPACE_DIR=$WORKSPACE_DIR
|
|
EOF
|
|
|
|
echo
|
|
echo "Wrote .env:"
|
|
cat .env
|
|
}
|
|
|
|
cmd_build() {
|
|
export BUILDX_NO_DEFAULT_ATTESTATIONS=1
|
|
docker compose build --no-cache --pull
|
|
}
|
|
|
|
cmd_start() {
|
|
echo "Preparing persistent directories and files under:"
|
|
echo " $DATA_DIR"
|
|
echo
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Home / Bash
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# /data/claudaris/home must be a directory.
|
|
ensure_dir "$DATA_DIR/home"
|
|
|
|
# .bashrc is bind-mounted to /root/.bashrc, so it MUST be a file.
|
|
if [ ! -e "$DATA_DIR/home/.bashrc" ]; then
|
|
cp "$REPO_DIR/files/bashrc" "$DATA_DIR/home/.bashrc"
|
|
elif [ ! -f "$DATA_DIR/home/.bashrc" ]; then
|
|
echo "ERROR: $DATA_DIR/home/.bashrc exists but is not a regular file." >&2
|
|
echo "Please remove or rename it, then run ./claudaris start again." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# bash_aliases is bind-mounted to /opt/dotfiles/bash_aliases, which is a
|
|
# regular file in the image, so the host source MUST also be a regular file.
|
|
if [ ! -e "$DATA_DIR/home/bash_aliases" ]; then
|
|
cp "$REPO_DIR/files/bash_aliases" "$DATA_DIR/home/bash_aliases"
|
|
elif [ ! -f "$DATA_DIR/home/bash_aliases" ]; then
|
|
echo "ERROR: $DATA_DIR/home/bash_aliases exists but is not a regular file." >&2
|
|
echo "Please remove or rename it, then run ./claudaris start again." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Claude Code
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ensure_dir "$DATA_DIR/claude"
|
|
|
|
# Both of these are individual file bind mounts. They MUST be files.
|
|
# Empty files are intentional on first run; Claude Code will populate them.
|
|
ensure_file "$DATA_DIR/claude/credentials.json"
|
|
ensure_file "$DATA_DIR/claude/claude.json"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OpenCode
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ensure_dir "$DATA_DIR/opencode"
|
|
|
|
# Individual file bind mount; MUST be a file.
|
|
ensure_file "$DATA_DIR/opencode/auth.json"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Optional SSH mount
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Optional: mount ~/.ssh into the container (read-only) for connecting out to
|
|
# other nodes. Opt in by creating $DATA_DIR/ssh and populating it with
|
|
# keys/config before starting the container.
|
|
#
|
|
# docker-compose.yml doesn't list this mount because Compose can't make a
|
|
# volume conditional on a host path existing. Instead, create a gitignored
|
|
# override file that Compose picks up automatically.
|
|
if [ -d "$DATA_DIR/ssh" ]; then
|
|
cat > "$REPO_DIR/docker-compose.override.yml" <<EOF
|
|
services:
|
|
claudaris:
|
|
volumes:
|
|
- $DATA_DIR/ssh:/root/.ssh:ro
|
|
EOF
|
|
elif [ -e "$DATA_DIR/ssh" ]; then
|
|
echo "ERROR: $DATA_DIR/ssh exists but is not a directory." >&2
|
|
echo "Please remove or rename it, then run ./claudaris start again." >&2
|
|
exit 1
|
|
else
|
|
rm -f "$REPO_DIR/docker-compose.override.yml"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Show mount sources before Docker starts.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
echo "Persistent paths:"
|
|
printf ' %-15s %s\n' "home/" "$DATA_DIR/home/"
|
|
printf ' %-15s %s\n' ".bashrc" "$DATA_DIR/home/.bashrc"
|
|
printf ' %-15s %s\n' "bash_aliases/" "$DATA_DIR/home/bash_aliases/"
|
|
printf ' %-15s %s\n' "credentials" "$DATA_DIR/claude/credentials.json"
|
|
printf ' %-15s %s\n' "claude.json" "$DATA_DIR/claude/claude.json"
|
|
printf ' %-15s %s\n' "auth.json" "$DATA_DIR/opencode/auth.json"
|
|
|
|
if [ -d "$DATA_DIR/ssh" ]; then
|
|
printf ' %-15s %s\n' "ssh/" "$DATA_DIR/ssh/"
|
|
fi
|
|
|
|
echo
|
|
echo "Starting container..."
|
|
|
|
docker compose up -d
|
|
}
|
|
|
|
cmd_connect() {
|
|
# The container exits on its own once the main tmux session ends (see
|
|
# entrypoint.sh) and isn't auto-restarted, so make sure it's up before
|
|
# exec'ing in — a no-op if it's already running. Plain bash rather than
|
|
# tmux here: when this container runs on a remote host, the caller is
|
|
# typically already inside a tmux session locally, and nesting tmux in
|
|
# tmux is annoying. tmux itself stays installed and available — run
|
|
# `tmux` manually inside the container if you want it.
|
|
docker compose up -d >/dev/null
|
|
docker compose exec claudaris bash
|
|
}
|
|
|
|
cmd_remove() {
|
|
docker compose down
|
|
}
|
|
|
|
case "${1:-help}" in
|
|
config|configure) cmd_config ;;
|
|
build) cmd_build ;;
|
|
start) cmd_start ;;
|
|
connect) cmd_connect ;;
|
|
remove|stop|rm) cmd_remove ;;
|
|
help|-h|--help) usage ;;
|
|
*)
|
|
echo "Unknown command: $1" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac |