summaryrefslogtreecommitdiff
path: root/link/pi.sh
blob: 5aec48f6169ba7e0763d889f5af4d2cb8ee4300f (plain)
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"