4b4b76e80e
Adds pi-atelier.json (defaults for the pi-atelier extension) and an install/uninstall step for it, so the config survives a devbox volume wipe and applies to pi on the host too — ~/.pi/agent is a named volume in a container, which the extension's own config path alone does not outlive. cp-if-absent rather than a symlink, unlike keybindings.json/AGENTS.md: the extension rewrites this exact path when the user saves from its menu (src/config.ts writeJsonAtomic -> "<path>.<pid>.tmp" then rename). rename(2) REPLACES a symlink with a regular file instead of following it, so a link would detach on the first menu save and the repo copy would quietly stop applying. Verified empirically before choosing the idiom, not assumed. Drift is respected in both directions, matching the pi-env.zsh shape: install never clobbers an existing file (warns + prints a diff hint), uninstall removes it only while its content still matches the repo. All five paths exercised against an isolated HOME: fresh copy, idempotent re-run, drift-preserved install, drift-preserved uninstall, matched-content uninstall. Values chosen for this setup and validated against the extension's own validateConfig (no warnings, no coercion): - segments drop "brand" (decoration, and first in the drop order anyway) - density compact — the rail relayouts at 132/96/72/56 cols and these sessions run over plain SSH at unknown width - contextWarning/Danger 60/85, earlier than the 70/90 default because defaultModel is opus-5 at xhigh thinking with obsmem compaction behind it - showSidebarToolNames true — MCP/tool-heavy sessions, names beat "something is running" - completionNotifications false — deliverSystemNotification only spawns for darwin/win32 and returns undefined on linux, and a container has no desktop session to reach anyway Docs: file inventory + install/uninstall tables in README.md and AGENTS.md, including why the symlink idiom does not apply here.
372 lines
15 KiB
Bash
Executable File
372 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# install.sh — install pi-toolkit
|
|
#
|
|
# Harness-side bring-up for the pi coding-agent (https://github.com/earendil-works/pi):
|
|
# - pi-env.zsh : shell loader sourcing ~/.config/pi/.env so AWS_PROFILE
|
|
# and AWS_REGION are in every shell that launches pi.
|
|
# - keybindings.json : mosh/tmux-friendly newline bindings
|
|
# (shift+enter, ctrl+j, alt+j).
|
|
# - pi-global-AGENTS.md : global agent instructions, symlinked to
|
|
# ~/.pi/agent/AGENTS.md so pi loads it at every start
|
|
# (e.g. "read the pi-extensions skill at session start").
|
|
# - settings.example.json : template for ~/.pi/agent/settings.json so pi
|
|
# starts without --provider/--model. Copy + edit.
|
|
# - pi-atelier.json : default Status Rail config for the pi-atelier
|
|
# extension (cp'd, not symlinked — see below).
|
|
#
|
|
# No dependency on MemPalace. For the palace bridge see
|
|
# https://gitea.jordbo.se/joakimp/mempalace-toolkit (optional).
|
|
#
|
|
# Idempotent. Safe to re-run. Non-destructive on drift: if installed content
|
|
# differs from repo, leaves user edits alone and points at diff.
|
|
|
|
set -euo pipefail
|
|
|
|
# ── locate self ──────────────────────────────────────
|
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
|
|
# ── targets ──────────────────────────────────────────
|
|
PI_EXT_DEST_DIR="${HOME}/.pi/agent/extensions" # existence = pi is installed
|
|
PI_AGENT_DIR="${HOME}/.pi/agent"
|
|
|
|
# keybindings — symlinked (pi doesn't rewrite it at runtime)
|
|
PI_KEYS_SRC="${SCRIPT_DIR}/keybindings.json"
|
|
PI_KEYS_DEST="${HOME}/.pi/agent/keybindings.json"
|
|
|
|
# global instructions — symlinked (pi doesn't rewrite AGENTS.md at runtime).
|
|
# pi loads ~/.pi/agent/AGENTS.md as global instructions at every startup.
|
|
PI_AGENTS_SRC="${SCRIPT_DIR}/pi-global-AGENTS.md"
|
|
PI_AGENTS_DEST="${HOME}/.pi/agent/AGENTS.md"
|
|
|
|
# settings template — NOT installed, only referenced (pi rewrites settings.json)
|
|
PI_SETTINGS_EXAMPLE="${SCRIPT_DIR}/settings.example.json"
|
|
PI_SETTINGS_DEST="${HOME}/.pi/agent/settings.json"
|
|
|
|
# pi-atelier Status Rail config — cp'd, NOT symlinked. pi-atelier rewrites this
|
|
# file at runtime ("Save as user default" in its menu, and sidebar/notification
|
|
# toggles save immediately) via a write-temp-then-rename, and rename(2) REPLACES
|
|
# a symlink with a regular file rather than following it — so a symlink would
|
|
# silently detach on the user's first menu save and the repo copy would stop
|
|
# taking effect. cp-if-absent + cmp drift check instead, same shape as
|
|
# pi-env.zsh.
|
|
PI_ATELIER_SRC="${SCRIPT_DIR}/pi-atelier.json"
|
|
PI_ATELIER_DEST="${HOME}/.pi/agent/pi-atelier.json"
|
|
|
|
# shell env loader — cp'd into oh-my-zsh custom dir (portability over symlink)
|
|
PI_ENV_SRC="${SCRIPT_DIR}/pi-env.zsh"
|
|
PI_ENV_OMZ_DEST="${HOME}/.oh-my-zsh/custom/pi-env.zsh"
|
|
|
|
# ── args ─────────────────────────────────────────────
|
|
ACTION="install"
|
|
ASSUME_YES="no"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--uninstall) ACTION="uninstall"; shift ;;
|
|
-y|--yes) ASSUME_YES="yes"; shift ;;
|
|
-h|--help)
|
|
cat <<EOF
|
|
install.sh — install pi-toolkit
|
|
|
|
Usage:
|
|
./install.sh # install (interactive confirm)
|
|
./install.sh --yes # install without prompt
|
|
./install.sh --uninstall # remove symlinks and copied loaders
|
|
|
|
What install does:
|
|
- Requires pi installed (~/.pi/agent/ must exist). Aborts cleanly otherwise.
|
|
- Symlinks keybindings.json into ~/.pi/agent/keybindings.json.
|
|
- Symlinks pi-global-AGENTS.md into ~/.pi/agent/AGENTS.md (global
|
|
instructions loaded by pi at every start).
|
|
- Copies pi-atelier.json into ~/.pi/agent/ if absent (Status Rail
|
|
defaults). cp not symlink: pi-atelier rewrites it on menu saves.
|
|
Existing file is left alone, with a diff hint.
|
|
- If oh-my-zsh is present (~/.oh-my-zsh/custom/), copies pi-env.zsh there
|
|
so every new zsh shell sources ~/.config/pi/.env. Uses cp (not symlink)
|
|
for dotfile-backup portability. Prints source snippet otherwise.
|
|
- Probes (non-halting warnings): settings.json present, and AWS_PROFILE /
|
|
AWS_REGION set if settings.json selects amazon-bedrock.
|
|
|
|
What uninstall does:
|
|
- Removes keybindings.json symlink if it points into this repo.
|
|
- Removes the AGENTS.md symlink if it points into this repo.
|
|
- Removes pi-env.zsh copy ONLY if its content still matches the repo.
|
|
- Removes pi-atelier.json ONLY if its content still matches the repo
|
|
(a menu save makes it yours — then it's left alone).
|
|
- Never touches settings.json (it's your file, not managed here).
|
|
|
|
No dependency on MemPalace. For memory integration see:
|
|
https://gitea.jordbo.se/joakimp/mempalace-toolkit
|
|
EOF
|
|
exit 0 ;;
|
|
*) echo "Unknown flag: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
# ── helpers ──────────────────────────────────────────
|
|
ok() { printf ' \e[32m✓\e[0m %s\n' "$*"; }
|
|
note() { printf '==> %s\n' "$*"; }
|
|
warn() { printf ' \e[33m!\e[0m %s\n' "$*" >&2; }
|
|
err() { printf ' \e[31m✗\e[0m %s\n' "$*" >&2; }
|
|
|
|
confirm() {
|
|
[[ "$ASSUME_YES" == "yes" ]] && return 0
|
|
read -r -p "Proceed? [y/N] " ans
|
|
[[ "$ans" =~ ^[Yy]$ ]]
|
|
}
|
|
|
|
link_if_into_repo() {
|
|
# Return 0 if $1 is a symlink pointing into $SCRIPT_DIR
|
|
local target
|
|
[[ -L "$1" ]] || return 1
|
|
target=$(readlink -f "$1")
|
|
[[ "$target" == "$SCRIPT_DIR"/* ]]
|
|
}
|
|
|
|
require_pi_installed() {
|
|
if [[ ! -d "$PI_AGENT_DIR" ]]; then
|
|
err "pi not detected at $PI_AGENT_DIR"
|
|
printf ' Install pi first: https://github.com/earendil-works/pi\n'
|
|
printf ' Re-run this installer after `pi` has been started at least once\n'
|
|
printf ' (first run creates ~/.pi/agent/).\n'
|
|
exit 4
|
|
fi
|
|
# Ensure the extensions dir exists so pi loads any extensions we may add
|
|
# later (e.g. mempalace-toolkit's mempalace.ts). pi creates this on first
|
|
# extension install but we may race ahead of that.
|
|
mkdir -p "$PI_EXT_DEST_DIR"
|
|
ok "pi detected at $PI_AGENT_DIR"
|
|
}
|
|
|
|
# ── install ──────────────────────────────────────────
|
|
install_keybindings() {
|
|
# Generic mosh/tmux newline fix. Symlinked so edits flow through git.
|
|
note "Linking keybindings.json → $PI_KEYS_DEST"
|
|
if [[ -e "$PI_KEYS_DEST" || -L "$PI_KEYS_DEST" ]]; then
|
|
if link_if_into_repo "$PI_KEYS_DEST"; then
|
|
ok "pi keybindings already linked"
|
|
return 0
|
|
fi
|
|
# Back up any existing real file / foreign symlink.
|
|
local backup="${PI_KEYS_DEST}.bak.$(date +%Y%m%d-%H%M%S)"
|
|
mv "$PI_KEYS_DEST" "$backup"
|
|
warn "Existing $PI_KEYS_DEST backed up to $backup"
|
|
fi
|
|
ln -s "$PI_KEYS_SRC" "$PI_KEYS_DEST"
|
|
ok "Linked keybindings.json → $PI_KEYS_SRC"
|
|
}
|
|
|
|
install_agents_md() {
|
|
# Global agent instructions. Symlinked so edits flow through git and the
|
|
# managed default survives container recreation (entrypoint re-runs this
|
|
# installer on every start). pi never rewrites AGENTS.md at runtime.
|
|
note "Linking pi-global-AGENTS.md → $PI_AGENTS_DEST"
|
|
if [[ -e "$PI_AGENTS_DEST" || -L "$PI_AGENTS_DEST" ]]; then
|
|
if link_if_into_repo "$PI_AGENTS_DEST"; then
|
|
ok "pi global AGENTS.md already linked"
|
|
return 0
|
|
fi
|
|
# Back up any existing real file / foreign symlink.
|
|
local backup="${PI_AGENTS_DEST}.bak.$(date +%Y%m%d-%H%M%S)"
|
|
mv "$PI_AGENTS_DEST" "$backup"
|
|
warn "Existing $PI_AGENTS_DEST backed up to $backup"
|
|
fi
|
|
ln -s "$PI_AGENTS_SRC" "$PI_AGENTS_DEST"
|
|
ok "Linked pi-global-AGENTS.md → $PI_AGENTS_SRC"
|
|
}
|
|
|
|
install_env_loader() {
|
|
# Copy pi-env.zsh into ~/.oh-my-zsh/custom/ (auto-loaded by omz).
|
|
# cp not symlink because that dir is typically part of a dotfiles backup
|
|
# and a symlink into this repo breaks on restore to another host.
|
|
# No oh-my-zsh → print a source snippet for manual rc-file addition.
|
|
|
|
if [[ -d "$HOME/.oh-my-zsh/custom" ]]; then
|
|
note "Installing pi-env.zsh into ~/.oh-my-zsh/custom/"
|
|
if [[ -f "$PI_ENV_OMZ_DEST" ]]; then
|
|
if cmp -s "$PI_ENV_SRC" "$PI_ENV_OMZ_DEST"; then
|
|
ok "pi-env.zsh already installed (content matches repo)"
|
|
return 0
|
|
fi
|
|
warn "$PI_ENV_OMZ_DEST exists and differs from repo copy"
|
|
printf ' Leaving your edits alone. Compare with:\n'
|
|
printf ' diff %q %q\n' "$PI_ENV_OMZ_DEST" "$PI_ENV_SRC"
|
|
printf ' To adopt the repo version: rm that file and re-run install.sh\n'
|
|
return 0
|
|
fi
|
|
cp "$PI_ENV_SRC" "$PI_ENV_OMZ_DEST"
|
|
ok "Copied pi-env.zsh → $PI_ENV_OMZ_DEST"
|
|
printf ' Open a new shell (or `exec zsh`) to load AWS_PROFILE / AWS_REGION.\n'
|
|
return 0
|
|
fi
|
|
|
|
# No oh-my-zsh — print shell-specific snippet.
|
|
note "oh-my-zsh not detected — manual shell setup needed"
|
|
local shell_name
|
|
shell_name="$(basename "${SHELL:-bash}")"
|
|
local rc_file
|
|
case "$shell_name" in
|
|
zsh) rc_file="~/.zshrc" ;;
|
|
bash) rc_file="~/.bashrc" ;;
|
|
*) rc_file="~/.${shell_name}rc # adjust for your shell" ;;
|
|
esac
|
|
printf ' Add this line to %s so every shell sources ~/.config/pi/.env:\n' "$rc_file"
|
|
printf ' source %q\n' "$PI_ENV_SRC"
|
|
printf ' (the loader itself is POSIX-compatible — works in bash and zsh.)\n'
|
|
}
|
|
|
|
install_atelier_config() {
|
|
# Status Rail defaults for the pi-atelier extension. cp-if-absent, never
|
|
# clobber: the extension writes this same path when the user saves from its
|
|
# menu, so an existing file is assumed to be the user's own preference.
|
|
# See the comment at PI_ATELIER_SRC for why this is a cp and not a symlink.
|
|
note "Installing pi-atelier.json → $PI_ATELIER_DEST"
|
|
if [[ -e "$PI_ATELIER_DEST" || -L "$PI_ATELIER_DEST" ]]; then
|
|
if cmp -s "$PI_ATELIER_SRC" "$PI_ATELIER_DEST"; then
|
|
ok "pi-atelier config already installed (content matches repo)"
|
|
return 0
|
|
fi
|
|
warn "$PI_ATELIER_DEST exists and differs from repo copy"
|
|
printf ' Leaving your settings alone (pi-atelier saves here too). Compare with:\n'
|
|
printf ' diff %q %q\n' "$PI_ATELIER_DEST" "$PI_ATELIER_SRC"
|
|
printf ' To adopt the repo defaults: rm that file and re-run install.sh\n'
|
|
return 0
|
|
fi
|
|
cp "$PI_ATELIER_SRC" "$PI_ATELIER_DEST"
|
|
ok "Copied pi-atelier.json → $PI_ATELIER_DEST"
|
|
printf ' Inert unless the pi-atelier extension is installed; read at session start.\n'
|
|
}
|
|
|
|
# ── Probes ───────────────────────────────────────────
|
|
# Never halt. warn + return 0 — mirrors opencode-toolkit and mempalace-toolkit.
|
|
check_pi_settings() {
|
|
# If settings.json missing, pi refuses to start without --provider/--model
|
|
# on every run. Template exists in this repo; point at the cp command.
|
|
if [[ -f "$PI_SETTINGS_DEST" ]]; then
|
|
ok "pi settings.json present at $PI_SETTINGS_DEST"
|
|
return 0
|
|
fi
|
|
|
|
warn "pi settings.json NOT found at $PI_SETTINGS_DEST"
|
|
printf ' Without it, pi must be invoked with --provider/--model every run.\n'
|
|
printf ' Bootstrap from the shipped template:\n'
|
|
printf ' cp %q %q\n' "$PI_SETTINGS_EXAMPLE" "$PI_SETTINGS_DEST"
|
|
printf ' $EDITOR %q # adjust region prefix + model IDs\n' "$PI_SETTINGS_DEST"
|
|
printf ' See README.md for the eu./us./anthropic: prefix rules.\n'
|
|
}
|
|
|
|
check_aws_env() {
|
|
# Only warn if settings.json selects amazon-bedrock. If pi uses a
|
|
# non-Bedrock provider (bare anthropic, openai, ...) AWS creds are
|
|
# irrelevant and this probe would be noise. If settings.json doesn't
|
|
# exist yet, check_pi_settings already told the user to bootstrap it
|
|
# — we can't know which provider they'll pick, so stay quiet here.
|
|
[[ -f "$PI_SETTINGS_DEST" ]] || return 0
|
|
grep -q '"amazon-bedrock"' "$PI_SETTINGS_DEST" 2>/dev/null || return 0
|
|
|
|
if [[ -n "${AWS_PROFILE:-}" && -n "${AWS_REGION:-}" ]]; then
|
|
ok "AWS env present (AWS_PROFILE=$AWS_PROFILE, AWS_REGION=$AWS_REGION)"
|
|
return 0
|
|
fi
|
|
|
|
warn "AWS_PROFILE and/or AWS_REGION not set in this shell"
|
|
printf ' pi with defaultProvider=amazon-bedrock needs both to invoke Bedrock.\n'
|
|
printf ' Recommended layout (matches tor-ms22 dotfiles pattern):\n'
|
|
printf ' ~/.config/pi/.env # AWS_PROFILE=..., AWS_REGION=...\n'
|
|
printf ' The pi-env.zsh loader (installed above) sources it automatically.\n'
|
|
printf ' Open a fresh shell to pick up the vars after creating the file.\n'
|
|
}
|
|
|
|
do_install() {
|
|
echo
|
|
echo "pi-toolkit installer"
|
|
echo "Repository: $SCRIPT_DIR"
|
|
echo
|
|
require_pi_installed
|
|
echo
|
|
echo "==> Installation plan:"
|
|
echo " Symlink keybindings.json → $PI_KEYS_DEST"
|
|
echo " Symlink pi-global-AGENTS.md → $PI_AGENTS_DEST"
|
|
echo " Copy pi-atelier.json → $PI_ATELIER_DEST (if absent)"
|
|
if [[ -d "$HOME/.oh-my-zsh/custom" ]]; then
|
|
echo " Copy pi-env.zsh → $PI_ENV_OMZ_DEST"
|
|
else
|
|
echo " Print manual source-snippet instructions for pi-env.zsh"
|
|
fi
|
|
echo
|
|
confirm || { echo "Aborted."; exit 0; }
|
|
echo
|
|
install_keybindings
|
|
echo
|
|
install_agents_md
|
|
echo
|
|
install_env_loader
|
|
echo
|
|
install_atelier_config
|
|
echo
|
|
check_pi_settings
|
|
echo
|
|
check_aws_env
|
|
echo
|
|
ok "Done."
|
|
}
|
|
|
|
# ── uninstall ────────────────────────────────────────
|
|
do_uninstall() {
|
|
echo
|
|
echo "pi-toolkit uninstaller"
|
|
echo "Repository: $SCRIPT_DIR"
|
|
echo
|
|
confirm || { echo "Aborted."; exit 0; }
|
|
echo
|
|
|
|
note "Removing pi keybindings symlink"
|
|
if link_if_into_repo "$PI_KEYS_DEST"; then
|
|
rm "$PI_KEYS_DEST"
|
|
ok "Removed pi keybindings symlink"
|
|
else
|
|
ok "No pi keybindings symlink to remove"
|
|
fi
|
|
|
|
echo
|
|
note "Removing pi global AGENTS.md symlink"
|
|
if link_if_into_repo "$PI_AGENTS_DEST"; then
|
|
rm "$PI_AGENTS_DEST"
|
|
ok "Removed pi global AGENTS.md symlink"
|
|
else
|
|
ok "No pi global AGENTS.md symlink to remove"
|
|
fi
|
|
|
|
echo
|
|
note "Removing pi-atelier.json (only if unmodified)"
|
|
if [[ -f "$PI_ATELIER_DEST" ]] && cmp -s "$PI_ATELIER_SRC" "$PI_ATELIER_DEST"; then
|
|
rm "$PI_ATELIER_DEST"
|
|
ok "Removed $PI_ATELIER_DEST"
|
|
elif [[ -f "$PI_ATELIER_DEST" ]]; then
|
|
warn "$PI_ATELIER_DEST differs from repo copy — left alone"
|
|
else
|
|
ok "No pi-atelier.json to remove"
|
|
fi
|
|
|
|
echo
|
|
note "Removing pi-env.zsh loader (oh-my-zsh path)"
|
|
# Only remove if content still matches the repo copy — user may have
|
|
# local edits we shouldn't silently discard.
|
|
if [[ -f "$PI_ENV_OMZ_DEST" ]] && cmp -s "$PI_ENV_SRC" "$PI_ENV_OMZ_DEST"; then
|
|
rm "$PI_ENV_OMZ_DEST"
|
|
ok "Removed $PI_ENV_OMZ_DEST"
|
|
elif [[ -f "$PI_ENV_OMZ_DEST" ]]; then
|
|
warn "$PI_ENV_OMZ_DEST differs from repo copy — left alone"
|
|
else
|
|
ok "No pi-env.zsh loader to remove"
|
|
fi
|
|
|
|
echo
|
|
ok "Done."
|
|
}
|
|
|
|
case "$ACTION" in
|
|
install) do_install ;;
|
|
uninstall) do_uninstall ;;
|
|
esac
|