mirror of
https://github.com/joelhooks/joelclaw.git
synced 2026-09-19 01:24:04 +08:00
306 lines
9.8 KiB
Bash
Executable File
306 lines
9.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Bootstrap a thin joelclaw satellite Machine.
|
|
# Run on the satellite itself, e.g. from Panda once SSH works:
|
|
# ssh joel@blaine 'bash -s' < scripts/setup-satellite-rig.sh
|
|
|
|
REPO_URL="${JOELCLAW_REPO_URL:-git@github.com:joelhooks/joelclaw.git}"
|
|
REPO_DIR="${JOELCLAW_REPO_DIR:-$HOME/Code/joelhooks/joelclaw}"
|
|
JC_SLACK_REPO_URL="${JC_SLACK_REPO_URL:-git@github.com:joelhooks/jc-slack.git}"
|
|
JC_SLACK_REPO_DIR="${JC_SLACK_REPO_DIR:-$HOME/Code/joelhooks/jc-slack}"
|
|
if [[ "$JC_SLACK_REPO_DIR" != /* ]]; then
|
|
JC_SLACK_REPO_DIR="$(pwd -P)/$JC_SLACK_REPO_DIR"
|
|
fi
|
|
CENTRAL_SSH="${JOELCLAW_CENTRAL_SSH:-joel@panda}"
|
|
CENTRAL_URL="${JOELCLAW_CENTRAL_URL:-http://joels-mac-studio.tail7af24.ts.net:3111}"
|
|
SATELLITE_TYPESENSE_URL="${TYPESENSE_URL:-${JOELCLAW_TYPESENSE_URL:-http://joels-mac-studio.tail7af24.ts.net:8108}}"
|
|
SATELLITE_DOCS_TYPESENSE_URL="${DOCS_TYPESENSE_URL:-${JOELCLAW_DOCS_TYPESENSE_URL:-http://joels-mac-studio.tail7af24.ts.net:8110}}"
|
|
SATELLITE_DOCS_API_URL="${DOCS_API_URL:-${JOELCLAW_DOCS_API_URL:-http://joels-mac-studio.tail7af24.ts.net:3838}}"
|
|
SKIP_GIT_SYNC="${JOELCLAW_SKIP_GIT_SYNC:-false}"
|
|
|
|
say() { printf '\n==> %s\n' "$*"; }
|
|
need() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
upsert_env_var() {
|
|
local file="$1"
|
|
local key="$2"
|
|
local value="$3"
|
|
[[ -n "$value" ]] || return 0
|
|
mkdir -p "$(dirname "$file")"
|
|
touch "$file"
|
|
chmod 600 "$file"
|
|
python3 - "$file" "$key" "$value" <<'PY'
|
|
import pathlib
|
|
import sys
|
|
|
|
path = pathlib.Path(sys.argv[1])
|
|
key = sys.argv[2]
|
|
value = sys.argv[3]
|
|
line = f"{key}={value}\n"
|
|
lines = path.read_text().splitlines(True) if path.exists() else []
|
|
replaced = False
|
|
out = []
|
|
for existing in lines:
|
|
if existing.startswith(f"{key}="):
|
|
out.append(line)
|
|
replaced = True
|
|
else:
|
|
out.append(existing)
|
|
if not replaced:
|
|
if out and not out[-1].endswith("\n"):
|
|
out[-1] += "\n"
|
|
out.append(line)
|
|
path.write_text("".join(out))
|
|
PY
|
|
}
|
|
|
|
ensure_skill_consumer_root() {
|
|
local root="$1"
|
|
local parent
|
|
parent="$(dirname "$root")"
|
|
mkdir -p "$parent"
|
|
|
|
if [[ -L "$root" ]]; then
|
|
local source_root
|
|
local composed_root
|
|
source_root="$(cd "$root" && pwd -P)"
|
|
composed_root="$(mktemp -d "$parent/.skills-compose.XXXXXX")"
|
|
while IFS= read -r -d '' entry; do
|
|
ln -s "$entry" "$composed_root/$(basename "$entry")"
|
|
done < <(find "$source_root" -mindepth 1 -maxdepth 1 -print0)
|
|
chmod 755 "$composed_root"
|
|
echo "Converting $root from whole-root symlink to composed skill root: $source_root"
|
|
unlink "$root"
|
|
mv "$composed_root" "$root"
|
|
elif [[ -e "$root" && ! -d "$root" ]]; then
|
|
echo "Cannot create skill consumer root: $root exists and is not a directory" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$root"
|
|
}
|
|
|
|
ensure_skill_pack_link() {
|
|
local root="$1"
|
|
local pack_name="$2"
|
|
local source_dir="$3"
|
|
local target="$root/$pack_name"
|
|
|
|
ensure_skill_consumer_root "$root"
|
|
|
|
if [[ -L "$target" ]]; then
|
|
local current_target
|
|
local resolved_source
|
|
local resolved_target
|
|
current_target="$(readlink "$target")"
|
|
resolved_source="$(cd "$source_dir" && pwd -P)"
|
|
if resolved_target="$(cd "$(dirname "$target")" && cd "$(dirname "$current_target")" 2>/dev/null && printf '%s/%s' "$(pwd -P)" "$(basename "$current_target")")" &&
|
|
[[ "$resolved_target" == "$resolved_source" ]]; then
|
|
return 0
|
|
fi
|
|
unlink "$target"
|
|
elif [[ -e "$target" ]]; then
|
|
echo "Cannot link skill pack: $target exists and is not a symlink" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ln -s "$source_dir" "$target"
|
|
}
|
|
|
|
say "satellite identity"
|
|
hostname
|
|
sw_vers -productVersion 2>/dev/null || true
|
|
uname -m
|
|
|
|
say "checking base tools"
|
|
missing=()
|
|
for tool in git python3 jq; do
|
|
need "$tool" || missing+=("$tool")
|
|
done
|
|
if ((${#missing[@]})); then
|
|
echo "Missing required tools: ${missing[*]}" >&2
|
|
echo "Install Xcode CLT/Homebrew basics first, then rerun." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! need bun; then
|
|
say "installing bun"
|
|
curl -fsSL https://bun.sh/install | bash
|
|
export PATH="$HOME/.bun/bin:$PATH"
|
|
fi
|
|
|
|
if need fnm; then
|
|
say "using fnm default node"
|
|
eval "$(fnm env --shell bash)"
|
|
fnm use default >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
if ! need node; then
|
|
say "installing node"
|
|
NODE_VERSION="${JOELCLAW_NODE_VERSION:-24.13.1}"
|
|
NODE_ARCH="arm64"
|
|
if [[ "$(uname -m)" == "x86_64" ]]; then
|
|
NODE_ARCH="x64"
|
|
fi
|
|
NODE_DIR="$HOME/.local/node-v$NODE_VERSION-darwin-$NODE_ARCH"
|
|
mkdir -p "$HOME/.local"
|
|
if [[ ! -x "$NODE_DIR/bin/node" ]]; then
|
|
curl -fsSL "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-$NODE_ARCH.tar.xz" | tar -xJ -C "$HOME/.local"
|
|
fi
|
|
mkdir -p "$HOME/.local/bin"
|
|
ln -sf "$NODE_DIR/bin/node" "$HOME/.local/bin/node"
|
|
ln -sf "$NODE_DIR/bin/npm" "$HOME/.local/bin/npm"
|
|
ln -sf "$NODE_DIR/bin/npx" "$HOME/.local/bin/npx"
|
|
export PATH="$HOME/.local/bin:$NODE_DIR/bin:$PATH"
|
|
fi
|
|
|
|
if ! need pnpm; then
|
|
say "installing pnpm"
|
|
if need corepack; then
|
|
corepack enable pnpm
|
|
elif need npm; then
|
|
npm install -g pnpm
|
|
elif need bun; then
|
|
bun install -g pnpm
|
|
export PATH="$HOME/.bun/bin:$PATH"
|
|
else
|
|
echo "Missing pnpm installer: need corepack, npm, or bun" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
say "materializing joelclaw repo"
|
|
mkdir -p "$(dirname "$REPO_DIR")"
|
|
if [[ ! -d "$REPO_DIR/.git" ]]; then
|
|
git clone "$REPO_URL" "$REPO_DIR"
|
|
elif [[ "$SKIP_GIT_SYNC" == "true" ]]; then
|
|
echo "Skipping git sync for existing repo because JOELCLAW_SKIP_GIT_SYNC=true"
|
|
else
|
|
git -C "$REPO_DIR" fetch origin main
|
|
git -C "$REPO_DIR" checkout main
|
|
git -C "$REPO_DIR" pull --ff-only origin main
|
|
fi
|
|
|
|
cd "$REPO_DIR"
|
|
REPO_DIR="$(pwd -P)"
|
|
|
|
say "installing workspace deps"
|
|
pnpm install
|
|
|
|
say "installing jc-slack"
|
|
mkdir -p "$(dirname "$JC_SLACK_REPO_DIR")"
|
|
if [[ ! -d "$JC_SLACK_REPO_DIR/.git" ]]; then
|
|
git clone "$JC_SLACK_REPO_URL" "$JC_SLACK_REPO_DIR"
|
|
elif [[ "$SKIP_GIT_SYNC" == "true" ]]; then
|
|
echo "Skipping jc-slack sync because JOELCLAW_SKIP_GIT_SYNC=true"
|
|
elif [[ -n "$(git -C "$JC_SLACK_REPO_DIR" status --porcelain)" ]]; then
|
|
echo "Skipping jc-slack sync because the checkout has local changes"
|
|
elif [[ "$(git -C "$JC_SLACK_REPO_DIR" branch --show-current)" != "main" ]]; then
|
|
echo "Skipping jc-slack sync because the checkout is not on main"
|
|
else
|
|
git -C "$JC_SLACK_REPO_DIR" fetch origin main
|
|
git -C "$JC_SLACK_REPO_DIR" pull --ff-only origin main
|
|
fi
|
|
(
|
|
cd "$JC_SLACK_REPO_DIR"
|
|
bun install --frozen-lockfile
|
|
)
|
|
mkdir -p "$HOME/.local/bin"
|
|
JC_SLACK_BIN_LINK="$HOME/.local/bin/jc-slack"
|
|
JC_SLACK_BIN_SOURCE="$JC_SLACK_REPO_DIR/bin/jc-slack"
|
|
if [[ -L "$JC_SLACK_BIN_LINK" ]]; then
|
|
if [[ "$(readlink "$JC_SLACK_BIN_LINK")" != "$JC_SLACK_BIN_SOURCE" ]]; then
|
|
echo "Cannot install jc-slack: $JC_SLACK_BIN_LINK points elsewhere" >&2
|
|
exit 1
|
|
fi
|
|
elif [[ -e "$JC_SLACK_BIN_LINK" ]]; then
|
|
echo "Cannot install jc-slack: $JC_SLACK_BIN_LINK exists and is not a symlink" >&2
|
|
exit 1
|
|
else
|
|
ln -s "$JC_SLACK_BIN_SOURCE" "$JC_SLACK_BIN_LINK"
|
|
fi
|
|
|
|
say "writing satellite environment"
|
|
ENV_FILE="$HOME/.config/system-bus.env"
|
|
upsert_env_var "$ENV_FILE" "JOELCLAW_CENTRAL_URL" "$CENTRAL_URL"
|
|
upsert_env_var "$ENV_FILE" "TYPESENSE_URL" "$SATELLITE_TYPESENSE_URL"
|
|
upsert_env_var "$ENV_FILE" "DOCS_TYPESENSE_URL" "$SATELLITE_DOCS_TYPESENSE_URL"
|
|
upsert_env_var "$ENV_FILE" "DOCS_API_URL" "$SATELLITE_DOCS_API_URL"
|
|
if [[ -n "${TYPESENSE_API_KEY:-}" ]]; then
|
|
upsert_env_var "$ENV_FILE" "TYPESENSE_API_KEY" "$TYPESENSE_API_KEY"
|
|
fi
|
|
if [[ -n "${PDF_BRAIN_API_TOKEN:-}" ]]; then
|
|
upsert_env_var "$ENV_FILE" "PDF_BRAIN_API_TOKEN" "$PDF_BRAIN_API_TOKEN"
|
|
fi
|
|
|
|
say "building joelclaw CLI"
|
|
mkdir -p "$HOME/.local/bin" "$HOME/.bun/bin"
|
|
bun build packages/cli/src/cli.ts --compile --outfile "$HOME/.bun/bin/joelclaw"
|
|
rm -f "$HOME/.local/bin/joelclaw"
|
|
cat > "$HOME/.local/bin/joelclaw" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
ENV_FILE="$HOME/.config/system-bus.env"
|
|
if [[ -f "$ENV_FILE" ]]; then
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
source "$ENV_FILE"
|
|
set +a
|
|
fi
|
|
exec "$HOME/.bun/bin/joelclaw" "$@"
|
|
SH
|
|
chmod 755 "$HOME/.local/bin/joelclaw"
|
|
export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH"
|
|
|
|
say "linking joelclaw runtime skill pack"
|
|
mkdir -p "$HOME/.pi/agent" "$HOME/.agents" "$HOME/.joelclaw"
|
|
ensure_skill_pack_link "$HOME/.pi/agent/skills" "joelclaw-runtime" "$REPO_DIR/skills"
|
|
ensure_skill_pack_link "$HOME/.agents/skills" "joelclaw-runtime" "$REPO_DIR/skills"
|
|
for root in \
|
|
"$HOME/.agents/skills" \
|
|
"$HOME/.pi/agent/skills" \
|
|
"$HOME/.claude/skills" \
|
|
"$HOME/.codex/skills" \
|
|
"$HOME/.cursor/skills"; do
|
|
ensure_skill_pack_link "$root" "slack-link" "$REPO_DIR/skills/slack-link"
|
|
done
|
|
|
|
say "ensuring Pi session directory exists"
|
|
mkdir -p "$HOME/.pi/agent/sessions"
|
|
|
|
if [[ -d "$REPO_DIR/.agents" ]]; then
|
|
say "linking repo identity files when present"
|
|
for name in IDENTITY.md SOUL.md ROLE.md USER.md TOOLS.md; do
|
|
if [[ -e "$REPO_DIR/.agents/$name" ]]; then
|
|
ln -sf "$REPO_DIR/.agents/$name" "$HOME/.joelclaw/$name" 2>/dev/null || true
|
|
fi
|
|
done
|
|
fi
|
|
|
|
say "checking satellite health"
|
|
joelclaw satellite health || true
|
|
|
|
say "checking Central relay path"
|
|
if ssh -o BatchMode=yes -o ConnectTimeout=8 "$CENTRAL_SSH" '
|
|
JOELCLAW=$(command -v joelclaw || true)
|
|
if [[ -z "$JOELCLAW" && -x "$HOME/.local/bin/joelclaw" ]]; then JOELCLAW="$HOME/.local/bin/joelclaw"; fi
|
|
if [[ -z "$JOELCLAW" && -x "$HOME/.bun/bin/joelclaw" ]]; then JOELCLAW="$HOME/.bun/bin/joelclaw"; fi
|
|
test -n "$JOELCLAW" && hostname
|
|
' >/tmp/joelclaw-central-check 2>/tmp/joelclaw-central-check.err; then
|
|
joelclaw satellite repair-request --central-ssh "$CENTRAL_SSH" --priority normal || true
|
|
else
|
|
echo "Central SSH check failed; repair-request skipped." >&2
|
|
cat /tmp/joelclaw-central-check.err >&2 || true
|
|
fi
|
|
|
|
cat <<'DONE'
|
|
|
|
Satellite bootstrap finished.
|
|
Manual checks still needed:
|
|
- `pi` installed and authenticated on this Machine
|
|
- Remote Login or Tailscale SSH enabled if Central should operate this Machine
|
|
- `joelclaw sessions search "test" --source local --machine $(hostname -s) --limit 1`
|
|
- `joelclaw sessions search "test" --source typesense --machine $(hostname -s) --runtime all --limit 1`
|
|
DONE
|