188 lines
No EOL
5.4 KiB
Bash
Executable file
188 lines
No EOL
5.4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# Set up shared agent state for agent-container.
|
|
#
|
|
# Moves agent state (Claude Code, Pi, OpenCode) from the real $HOME into the
|
|
# container home and symlinks it back, so the containerized tools (claude, pi,
|
|
# opencode) and the local variants (claude-local, pi-local, opencode-local)
|
|
# share logins, settings, and history. See README.md, section "Sharing agent
|
|
# state between host and container via symlinks".
|
|
#
|
|
# Shows a plan and asks for confirmation before changing anything:
|
|
# --dry-run (-n) show the plan without changing anything
|
|
# --yes (-y) apply the plan without asking
|
|
|
|
set -euo pipefail
|
|
|
|
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
CONTAINER_HOME="$XDG_DATA_HOME/agent-container/container-home"
|
|
|
|
# State directories to share. Created in the container home (and linked into
|
|
# $HOME) even if they don't exist yet, so both sides share from the start.
|
|
DIRS=(
|
|
.claude
|
|
.pi
|
|
.agents
|
|
.config/opencode
|
|
.local/share/opencode
|
|
)
|
|
|
|
# Single files to share. Only linked when they exist on one side, tools create
|
|
# them on first use.
|
|
FILES=(
|
|
.claude.json
|
|
)
|
|
|
|
info() { printf ' %s\n' "$*"; }
|
|
warn() { printf ' warning: %s\n' "$*" >&2; }
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: setup.sh [--dry-run] [--yes]
|
|
|
|
Shares agent state (.claude, .pi, .agents, OpenCode state, ...) between the
|
|
host \$HOME and the agent-container container home by moving it into the
|
|
container home and symlinking back from \$HOME. Shows a plan and asks for
|
|
confirmation before changing anything.
|
|
|
|
--dry-run, -n show the plan without changing anything
|
|
--yes, -y apply the plan without asking
|
|
EOF
|
|
}
|
|
|
|
# Decide what would be done with an item and print the action name.
|
|
# Pure: changes nothing, so the plan can be computed up front.
|
|
decide() {
|
|
local item="$1" must_exist="$2"
|
|
local host="$HOME/$item" target="$CONTAINER_HOME/$item"
|
|
|
|
if [ -L "$host" ]; then
|
|
case "$(readlink -f "$host")" in
|
|
"$CONTAINER_HOME"/*) echo ok ;;
|
|
*) echo foreign ;;
|
|
esac
|
|
return
|
|
fi
|
|
|
|
if [ -e "$host" ] && [ -e "$target" ]; then
|
|
echo conflict
|
|
return
|
|
fi
|
|
|
|
if [ ! -e "$host" ] && [ ! -e "$target" ]; then
|
|
if [ "$must_exist" = "yes" ]; then
|
|
echo skip
|
|
else
|
|
echo created
|
|
fi
|
|
return
|
|
fi
|
|
|
|
if [ -e "$host" ]; then
|
|
echo moved
|
|
else
|
|
echo linked
|
|
fi
|
|
}
|
|
|
|
# Explain an action; used for both the plan and the applied result.
|
|
describe() {
|
|
local action="$1" item="$2"
|
|
case "$action" in
|
|
moved) info "moved $item into the container home, symlinked from \$HOME" ;;
|
|
linked) info "linked $item (container home already had it)" ;;
|
|
created) info "created $item in the container home, symlinked from \$HOME" ;;
|
|
ok) info "ok $item (already shared)" ;;
|
|
skip) info "skip $item (does not exist yet, the tool will create it)" ;;
|
|
foreign) warn "$item is a symlink to $(readlink "$HOME/$item"), not touching it" ;;
|
|
conflict)
|
|
warn "$item exists in both \$HOME and the container home"
|
|
warn "reconcile the two copies manually, then re-run this script"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
_link() {
|
|
mkdir -p "$(dirname "$1")"
|
|
ln -s "$2" "$1"
|
|
}
|
|
|
|
# Apply one planned action, then report it.
|
|
apply() {
|
|
local item="$1" action="$2"
|
|
local host="$HOME/$item" target="$CONTAINER_HOME/$item"
|
|
|
|
case "$action" in
|
|
created) mkdir -p "$target" ;;
|
|
moved)
|
|
mkdir -p "$(dirname "$target")"
|
|
mv "$host" "$target"
|
|
;;
|
|
esac
|
|
case "$action" in
|
|
created|moved|linked) _link "$host" "$target" ;;
|
|
esac
|
|
describe "$action" "$item"
|
|
}
|
|
|
|
main() {
|
|
local dry_run=no ask=yes
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--dry-run|-n) dry_run=yes; ask=no ;;
|
|
--yes|-y) ask=no ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) usage >&2; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
printf 'Sharing agent state via %s\n\n' "$CONTAINER_HOME"
|
|
|
|
# Plan everything up front; decide() changes nothing, so applying the
|
|
# stored actions afterwards is exactly what the plan describes.
|
|
local -a items=() actions=()
|
|
local item action
|
|
for item in "${DIRS[@]}"; do items+=("$item"); actions+=("$(decide "$item" no)"); done
|
|
for item in "${FILES[@]}"; do items+=("$item"); actions+=("$(decide "$item" yes)"); done
|
|
|
|
printf 'Plan:\n'
|
|
local i
|
|
for i in "${!items[@]}"; do
|
|
describe "${actions[$i]}" "${items[$i]}"
|
|
done
|
|
|
|
if [ "$dry_run" = "yes" ]; then
|
|
printf '\nDry run: nothing changed.\n'
|
|
return
|
|
fi
|
|
|
|
if [ "$ask" = "yes" ]; then
|
|
if [ -t 0 ]; then
|
|
local reply
|
|
printf '\nApply these changes? [y/N] '
|
|
read -r reply
|
|
case "$reply" in
|
|
y|Y|yes|Yes) ;;
|
|
*) printf 'Aborted, nothing changed.\n'; return ;;
|
|
esac
|
|
else
|
|
printf '\nNot running interactively; pass --yes to apply the plan.\n' >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
printf '\n'
|
|
for i in "${!items[@]}"; do
|
|
apply "${items[$i]}" "${actions[$i]}"
|
|
done
|
|
|
|
if [ -f "$HOME/.gitconfig" ] && [ ! -e "$CONTAINER_HOME/.gitconfig" ]; then
|
|
printf '\n'
|
|
info "Tip: share your git config too (see README):"
|
|
info " ln $HOME/.gitconfig $CONTAINER_HOME/.gitconfig"
|
|
fi
|
|
}
|
|
|
|
main "$@" |