diff --git a/claudaris b/claudaris index 865fbdc..6dccbcd 100755 --- a/claudaris +++ b/claudaris @@ -33,6 +33,46 @@ Commands: 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 @@ -71,42 +111,67 @@ cmd_build() { } 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" + echo "Preparing persistent directories and files under:" + echo " $DATA_DIR" + echo - # 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" + # --------------------------------------------------------------------------- + # Home / Bash + # --------------------------------------------------------------------------- - # 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" + # /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 (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. + # 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" <&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 } @@ -149,4 +237,4 @@ case "${1:-help}" in usage >&2 exit 1 ;; -esac +esac \ No newline at end of file