#!/bin/bash # pi's settings.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_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"