all repos — dot @ 611dfd90d4e523984a78728ebfee033e4c16b37d

Shell, editor and terminal configuration, and the tools that go with them

link/pi.sh (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
#!/bin/bash

# pi's extensions directory, and its settings.json and models.json, generated.
# Dry run by default; pass `yes` to apply.

set -euo pipefail

# shellcheck source-path=SCRIPTDIR
# shellcheck source=lib.sh
source "$(dirname "$0")/lib.sh" "$@"

# ~/.pi/agent/settings.json is a real file built from config/pi/settings.json
# plus every ~/.config/pi/settings.d/*.json that another layer links in. It
# cannot be a symlink into a repo: pi has no second global settings file to
# layer with, and it writes to this one at runtime (lastChangelogVersion,
# /model, `pi install`), which used to dirty the tracked file.
#
# Merging the sources: list keys (packages, extensions, skills, prompts,
# themes) are concatenated in file order, a package deduplicated by its source
# (an object entry beats a bare string); any other key takes the last file's
# value. Relative paths in a fragment's `extensions`, `skills`, `prompts` and
# `themes` resolve against the fragment's real location, so a layer can point
# at files in its own checkout.
#
# Merging with what is already there is three-way, against what this script
# generated last time (kept in ~/.local/state/dotfiles/pi-settings.json): a key
# -- or a list entry, a package by its source -- that pi or the operator has not
# changed since then takes the new generated value, removal included; one they
# changed keeps their value. With no record of a last run, what is there is kept
# and what is missing is added. So a /model choice or a `pi install` survives
# relinks, and a package dropped from a repo really goes. Keys nothing generates
# (lastChangelogVersion) are simply kept.
#
# Written under pi's own settings lock, via a temporary file and a rename; a run
# that finds the lock held skips and says so, and the next link catches up.
#
# Run after other layers' link.sh, so their fragments are in place.
pi_dir="$HOME/.pi/agent"
if [[ ! -d "$pi_dir" ]]
then
  exit 0
fi

# pi's extensions: a real directory, with this repo's tracked extensions linked
# in one by one, so an extension only one machine has -- herdr's pi integration
# writes herdr-agent-state.ts here -- sits beside them instead of landing in this
# checkout. pi loads every *.ts and *.js there and follows links, and it skips a
# dangling one silently (resource-loader.js, pi 1.0.0), so the link left behind
# by an extension deleted from the repo does no harm.
#
# Machines used to link the whole directory to the repo. That link goes first:
# linking into it would write through to the repo and turn its files into links
# to themselves. Tracked files only, so nothing a machine left in the repo's
# directory is linked back in.
pi_extensions="$pi_dir/extensions"
if [[ -L "$pi_extensions" ]]
then
  if [[ -n "${is_dry_run}" ]]
  then
    yellow "$pi_extensions is a link to the repo; will become a directory of links to its tracked files"
  else
    rm "$pi_extensions"
  fi
fi
if [[ ! -L "$pi_extensions" ]]
then
  [[ -n "${is_dry_run}" ]] || mkdir -p "$pi_extensions"
  while IFS= read -r -d '' f
  do
    link "$dotfiles_dir/$f" "$pi_extensions/$(basename "$f")"
  done < <(git -C "$dotfiles_dir" ls-files -z -- config/pi/extensions)
fi

pi_status="$(DRY_RUN="$is_dry_run" python3 "$dotfiles_dir/link/pi-settings.py" \
  "$config_dir/pi/settings.json" \
  "$xdg_config_dir/pi/settings.d" \
  "$pi_dir/settings.json" \
  "${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/pi-settings.json" \
  "$config_dir")"

case "${pi_status%%:*}" in
  wrote)       green "pi settings: ${pi_status#*:}" ;;
  unchanged)   green "pi settings: unchanged" ;;
  would-write) yellow "pi settings will be generated: ${pi_status#*:}" ;;
  locked)      yellow "pi settings: pi holds its settings lock; left as is, the next link catches up" ;;
  error)       red "pi settings: ${pi_status#*:}" ;;
esac

# pi's models.json, generated too. It used to be a plain symlink to the tracked
# file, which left a machine whose models come from somewhere other than a
# vendor's own API with nowhere to say so: pi reads providers from exactly one
# path (join(getAgentDir(), "models.json")), and a `providers` block in a
# settings fragment is inert, since the settings schema has no such key.
#
# So the same shape as the settings above: the repo's file, then every
# ~/.config/pi/models.d/*.json another layer or this machine puts there. Unlike
# the settings this is a plain generated file with no three-way merge -- pi
# never writes it, keeping its runtime state in the sibling models-store.json --
# so what lands is whatever the repo and the fragments say. A hand-edit of
# ~/.pi/agent/models.json does not survive a relink; put it in a fragment.
pi_models_out="$(DRY_RUN="$is_dry_run" python3 "$dotfiles_dir/link/pi-models.py" \
  "$config_dir/pi/models.json" \
  "$xdg_config_dir/pi/models.d" \
  "$pi_dir/models.json")"

while IFS= read -r line
do
  case "${line%%:*}" in
    note)        yellow "pi models: ${line#*:}" ;;
    wrote)       green "pi models: ${line#*:}" ;;
    unchanged)   green "pi models: unchanged" ;;
    would-write) yellow "pi models will be generated: ${line#*:}" ;;
    error)       red "pi models: ${line#*:}" ;;
  esac
done <<< "$pi_models_out"