summaryrefslogtreecommitdiff
path: root/link/pi.sh
diff options
context:
space:
mode:
Diffstat (limited to 'link/pi.sh')
-rwxr-xr-xlink/pi.sh243
1 files changed, 4 insertions, 239 deletions
diff --git a/link/pi.sh b/link/pi.sh
index 22ecd96..0fde248 100755
--- a/link/pi.sh
+++ b/link/pi.sh
@@ -40,187 +40,12 @@ then
exit 0
fi
-pi_status="$(DRY_RUN="$is_dry_run" python3 - \
+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" <<'PY'
-import glob, json, os, sys
-
-base_path, frag_dir, target, state_path, config_dir = sys.argv[1:6]
-LIST_KEYS = ("packages", "extensions", "skills", "prompts", "themes")
-PATH_KEYS = ("extensions", "skills", "prompts", "themes")
-
-
-def load(path):
- with open(path) as f:
- return json.load(f)
-
-
-def source(entry):
- return entry if isinstance(entry, str) else entry.get("source")
-
-
-def add_packages(into, entries):
- for entry in entries:
- key = source(entry)
- existing = next((i for i, e in enumerate(into) if source(e) == key), None)
- if existing is None:
- into.append(entry)
- elif not isinstance(entry, str):
- into[existing] = entry
-
-
-# What the repos want now.
-desired = {}
-for path in [base_path] + sorted(glob.glob(os.path.join(frag_dir, "*.json"))):
- try:
- layer = load(path)
- except (OSError, ValueError) as e:
- print("error:cannot read %s: %s" % (path, e))
- sys.exit(0)
- here = os.path.dirname(os.path.realpath(path))
- for key, value in layer.items():
- if key in PATH_KEYS and path != base_path:
- value = [v if os.path.isabs(os.path.expanduser(v)) else os.path.normpath(os.path.join(here, v))
- for v in value]
- if key == "packages":
- add_packages(desired.setdefault("packages", []), value)
- elif key in LIST_KEYS:
- merged = desired.setdefault(key, [])
- merged.extend(v for v in value if v not in merged)
- else:
- desired[key] = value
-
-# What is there. A symlink is the pre-generation state: its target is a tracked
-# file this repo now owns, so treat it as already matching.
-def snapshot():
- """What is on disk now, to notice a write that lands before the lock."""
- try:
- return (os.path.islink(target), open(target).read())
- except OSError:
- return (os.path.islink(target), None)
-
-
-on_disk = snapshot()
-first_run = False
-if os.path.islink(target):
- first_run = True
- try:
- local = load(target)
- except (OSError, ValueError):
- local = {}
- local = {k: v for k, v in local.items() if k not in desired and k not in LIST_KEYS and k != "packages"}
- local.update(json.loads(json.dumps(desired)))
- last = desired
-else:
- try:
- local = load(target)
- except FileNotFoundError:
- local = {}
- except ValueError as e:
- print("error:%s is not valid JSON, leaving it: %s" % (target, e))
- sys.exit(0)
- # No record of a previous run -- a new machine where pi made its own file
- # first, or lost state: nothing is known to have come from the repos, so
- # keys already there are kept and keys missing arrive.
- try:
- last = load(state_path)
- except (OSError, ValueError):
- last = {}
-
-MISSING = object()
-
-
-def three_way(ours, before, theirs):
- """ours: on disk; before: generated last time; theirs: generated now."""
- if ours == before:
- return theirs
- return ours
-
-
-def merge_list(key, identity):
- """Per entry, three-way, in generated order then local-only extras."""
- ours = {identity(e): e for e in local.get(key, [])}
- before = {identity(e): e for e in last.get(key, [])}
- theirs = {identity(e): e for e in desired.get(key, [])}
- merged = []
- for ident in [identity(e) for e in desired.get(key, [])] + \
- [identity(e) for e in local.get(key, []) if identity(e) not in theirs]:
- value = three_way(ours.get(ident, MISSING), before.get(ident, MISSING), theirs.get(ident, MISSING))
- if value is not MISSING:
- merged.append(value)
- return merged
-
-
-result = {}
-for key in list(local) + [k for k in desired if k not in local]:
- if key in LIST_KEYS:
- merged = merge_list(key, source if key == "packages" else (lambda e: e))
- if merged or key in local:
- result[key] = merged
- continue
- value = three_way(local.get(key, MISSING), last.get(key, MISSING), desired.get(key, MISSING))
- if value is not MISSING:
- result[key] = value
-
-changed = first_run or result != local
-lock = target + ".lock"
-
-
-def take_lock():
- """pi's own lock (proper-lockfile: a directory, stale after 10s), so neither
- side can read, then write over the other's change."""
- import time
- for _ in range(2):
- try:
- os.mkdir(lock)
- return True
- except FileExistsError:
- try:
- if time.time() - os.stat(lock).st_mtime > 10:
- os.rmdir(lock)
- continue
- except FileNotFoundError:
- continue
- return False
- return False
-
-
-def save_state():
- os.makedirs(os.path.dirname(state_path), exist_ok=True)
- with open(state_path + ".tmp", "w") as f:
- json.dump(desired, f, indent=2)
- os.replace(state_path + ".tmp", state_path)
-
-
-if not changed:
- print("unchanged:")
- if not os.environ.get("DRY_RUN"):
- save_state()
-elif os.environ.get("DRY_RUN"):
- print("would-write:" + ("replacing the link" if first_run else "merged changes"))
-elif not take_lock():
- print("locked:")
-elif snapshot() != on_disk:
- # pi wrote between the read above and taking the lock: merge again next time
- # rather than write over it.
- os.rmdir(lock)
- print("locked:")
-else:
- try:
- tmp = target + ".dotfiles-tmp"
- with open(tmp, "w") as f:
- json.dump(result, f, indent=2)
- f.write("\n")
- os.replace(tmp, target)
- save_state()
- finally:
- os.rmdir(lock)
- print("wrote:" + ("replaced the link" if first_run else "merged changes"))
-PY
-)"
+ "$config_dir")"
case "${pi_status%%:*}" in
wrote) green "pi settings: ${pi_status#*:}" ;;
@@ -242,70 +67,10 @@ esac
# 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 - \
+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" <<'PY'
-import glob, json, os, sys
-
-base_path, frag_dir, target = sys.argv[1:4]
-
-
-def load(path):
- with open(path) as f:
- return json.load(f)
-
-
-# The repo's providers, then each fragment's. A fragment may add a provider or
-# replace one of the same name; any other top-level key takes the last file's
-# value. Which provider came from where is reported: a fragment quietly
-# shadowing a provider the repo ships would otherwise be invisible.
-desired = {}
-notes = []
-for path in [base_path] + sorted(glob.glob(os.path.join(frag_dir, "*.json"))):
- try:
- layer = load(path)
- except (OSError, ValueError) as e:
- print("error:cannot read %s: %s" % (path, e))
- sys.exit(0)
- for key, value in layer.items():
- if key == "providers":
- providers = desired.setdefault("providers", {})
- if path != base_path:
- for name in value:
- notes.append("%s %s by %s" % (
- name,
- "replaced" if name in providers else "added",
- os.path.basename(path),
- ))
- providers.update(value)
- else:
- desired[key] = value
-
-# A symlink is the pre-generation state: replace it, even with no fragments, so
-# the live file is never the tracked one.
-was_link = os.path.islink(target)
-try:
- current = None if was_link else load(target)
-except (OSError, ValueError):
- current = None
-
-for note in notes:
- print("note:" + note)
-
-if current == desired:
- print("unchanged:")
-elif os.environ.get("DRY_RUN"):
- print("would-write:" + ("replacing the link" if was_link else "merged changes"))
-else:
- tmp = target + ".dotfiles-tmp"
- with open(tmp, "w") as f:
- json.dump(desired, f, indent=2)
- f.write("\n")
- os.replace(tmp, target)
- print("wrote:" + ("replaced the link" if was_link else "merged changes"))
-PY
-)"
+ "$pi_dir/models.json")"
while IFS= read -r line
do