Files
2026-07-21 02:17:06 -07:00

153 lines
5.6 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
}
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() {
# Seed host copies of .bashrc and bash_aliases, without clobbering any
# customization already made on this host. Both are bind-mounted as
# single files below so they can be tweaked per-host (e.g. a coworker
# importing their own aliases) without rebuilding the image.
mkdir -p "$DATA_DIR/home"
[ -f "$DATA_DIR/home/.bashrc" ] || cp "$REPO_DIR/files/bashrc" "$DATA_DIR/home/.bashrc"
[ -f "$DATA_DIR/home/bash_aliases" ] || cp "$REPO_DIR/files/bash_aliases" "$DATA_DIR/home/bash_aliases"
# Two files carry everything needed to stay logged in: .credentials.json
# (the OAuth token) and claude.json (account/onboarding state — Claude Code
# checks this too, so persisting the token alone isn't enough to avoid a
# re-login prompt after a rebuild). Each is bind-mounted individually,
# same as .bashrc above, seeded empty on first run so Docker doesn't create
# a directory in its place. Individual file mounts (rather than a directory
# volume + in-container symlink) matter here: Claude Code likely saves
# these atomically (write a temp file, then rename() over the target), and
# rename() onto a symlink replaces the symlink instead of writing through
# it — silently breaking persistence after the first write. A bind mount
# doesn't have that failure mode.
mkdir -p "$DATA_DIR/claude"
[ -f "$DATA_DIR/claude/credentials.json" ] || touch "$DATA_DIR/claude/credentials.json"
[ -f "$DATA_DIR/claude/claude.json" ] || touch "$DATA_DIR/claude/claude.json"
# OpenCode stores provider credentials (API keys/OAuth tokens) in a single
# auth.json, same atomic-write concern as Claude Code above, so it gets the
# same individual-file bind mount treatment.
mkdir -p "$DATA_DIR/opencode"
[ -f "$DATA_DIR/opencode/auth.json" ] || touch "$DATA_DIR/opencode/auth.json"
# 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 (compose can't make a volume conditional on a host path
# existing), so it's added via a gitignored override file that compose
# picks up automatically when present, and removed when the opt-in host
# dir isn't there.
if [ -d "$DATA_DIR/ssh" ]; then
cat > "$REPO_DIR/docker-compose.override.yml" <<EOF
services:
claudaris:
volumes:
- $DATA_DIR/ssh:/root/.ssh:ro
EOF
else
rm -f "$REPO_DIR/docker-compose.override.yml"
fi
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