a6abe17edc
pi auto-loads every .ts in ~/.pi/agent/extensions/, which lives in the devbox-pi-config volume. A baked extension only seeds a fresh volume once, and a hand-copied one is a per-device step that drifts — the same two problems link-pi-models.sh was added for on 2026-10-12, one level down. This is also the only path that reaches every host: four of them mount common/pi from docker-compose-repo, and three cannot be reached from a devbox to create a symlink by hand (measured: no ssh config entry, no name resolution, and this instance's ~/.ssh-local/config carries no jump alias — only gitea and the mempalace feed host are reachable). link-pi-extensions.sh links each TOP-LEVEL *.ts from the mounted fleet directory (default /opt/fleet-pi, the same directory that already carries models.json; PI_EXTENSIONS_SRC overrides, =off disables). A symlink, not a single-file bind mount, for the measured reason already documented for models.json: git pull replaces a file by rename, and a file mount then serves stale content. Never destructive — a real file of that name in ~/.pi/agent/extensions/ wins over the fleet copy. Nested *.ts are deliberately not linked: the directory is fleet pi config, not an extension tree. - scripts/test-link-pi-extensions.sh: 16 two-sided cases (the negative half is the contract), all passing, wired into lint.yml beside the models one. - smoke-test.sh and recreate-sanity-check.sh gained the matching in-image and on-device checks, each gated on the fleet directory actually holding *.ts so a device without a shared directory is not judged by them. - README: the shared directory can carry extensions, and the rules that apply. Also repairs a pre-existing release blocker, found while checking this change. check-doc-drift.sh failed on pristine HEAD (verified in a clean worktree): pi-extensions main moved 0d8e417 -> 4c1d28d without the SHA being named above the v1.11.2 heading, and docker-publish.yml's resolve-versions has needs: [lint-gate] — so the next tag could not have published. The CHANGELOG now names 4c1d28d and describes the two confirm-destructive commits from their diff (they stop the gate prompting on harmless redirects, and add tee/dd of= as write forms); node --test test/confirm-destructive.test.mjs at 4c1d28d: 2 pass, 0 fail. Local verification: lint-shell.sh (21 files, severity error), check-workflow-shell.sh, check-skill-floor.sh, check-dockerfile-directives.sh, check-doc-drift.sh, and both link-pi-*.sh contract suites all pass. actionlint and hadolint are not installed in a devbox, so those CI steps are unvalidated here; the smoke-test addition runs only inside the image at release time.
76 lines
3.1 KiB
Bash
76 lines
3.1 KiB
Bash
#!/usr/bin/env bash
|
|
# link-pi-extensions.sh — point ~/.pi/agent/extensions/*.ts at shared, mounted files.
|
|
#
|
|
# pi auto-loads every .ts file in ~/.pi/agent/extensions/. In pi-devbox that
|
|
# directory lives in the devbox-pi-config named volume, which makes both obvious
|
|
# ways of providing a fleet extension wrong — the same two ways as models.json
|
|
# (see link-pi-models.sh, whose header this mirrors):
|
|
# - baked into the image, it would only seed a FRESH volume, once;
|
|
# - copied by hand into each volume, it is one more per-device step, and the
|
|
# copies drift the first time the extension changes.
|
|
# Instead, keep the .ts files in the same host directory that already holds
|
|
# models.json, mount that directory read-only, and let this script link them in
|
|
# at every container start:
|
|
#
|
|
# volumes:
|
|
# - ./pi-shared:/opt/fleet-pi:ro # a directory whose top-level *.ts are extensions
|
|
#
|
|
# A SYMLINK, not a single-file bind mount, and for the measured reason given in
|
|
# link-pi-models.sh: `git pull` replaces a file by rename, so a single-file mount
|
|
# keeps the old inode and serves stale content until the next recreate. A link
|
|
# follows the path, so a pulled change reaches the next pi session.
|
|
#
|
|
# Only TOP-LEVEL *.ts files are linked — no recursive search. The directory is
|
|
# fleet-shared pi config: models.json, its own docs and any sub-checkout are not
|
|
# extensions, and silently loading them would be worse than missing one.
|
|
#
|
|
# PI_EXTENSIONS_SRC overrides the directory (default /opt/fleet-pi).
|
|
# PI_EXTENSIONS_SRC=off (or empty) disables this script.
|
|
#
|
|
# Never destructive, per file. It only ever creates a link where nothing exists:
|
|
# - no file at the destination → link created
|
|
# - the destination is already ours → nothing
|
|
# - a real file with that name exists → left alone; one NOTE line says why
|
|
# (the user's own extension of that name deliberately wins over the fleet copy)
|
|
# - the destination links elsewhere → left alone; a deliberate choice
|
|
# A link this script made whose target later disappears (mount removed) is
|
|
# harmless and needs no cleanup: pi reports the unreadable extension and the rest
|
|
# of the session is unaffected.
|
|
#
|
|
# Called by entrypoint-user.sh. Safe to run by hand at any time.
|
|
set -u
|
|
|
|
src="${PI_EXTENSIONS_SRC-/opt/fleet-pi}"
|
|
case "$src" in
|
|
"" | off | OFF | Off) exit 0 ;;
|
|
esac
|
|
[ -d "$src" ] || exit 0
|
|
|
|
agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
dest_dir="$agent_dir/extensions"
|
|
|
|
for f in "$src"/*.ts; do
|
|
[ -f "$f" ] || continue
|
|
name="$(basename "$f")"
|
|
dest="$dest_dir/$name"
|
|
|
|
if [ -L "$dest" ]; then
|
|
# Ours already, or someone else's deliberate link: either way, nothing to do.
|
|
[ "$(readlink "$dest")" = "$f" ] || \
|
|
echo "NOTE: $dest links to $(readlink "$dest"), not $f; left alone"
|
|
continue
|
|
fi
|
|
if [ -e "$dest" ]; then
|
|
echo "NOTE: $dest is a real file; not linking $f (move it away to use the fleet copy)"
|
|
continue
|
|
fi
|
|
|
|
mkdir -p "$dest_dir" 2>/dev/null || true
|
|
if ln -s "$f" "$dest" 2>/dev/null; then
|
|
echo "pi extension: linked $name -> $f"
|
|
else
|
|
echo "WARN: could not link $dest -> $f (continuing)"
|
|
fi
|
|
done
|
|
exit 0
|