diff options
Diffstat (limited to 'link/pi.sh')
| -rwxr-xr-x | link/pi.sh | 243 |
1 files changed, 4 insertions, 239 deletions
@@ -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 |
