ryansch

ryansch / plannotator-update

Last active 1 month ago

Like 0

Revision b576ff8ba2dd4cfa816db1d3b84a43fc3fe9a407

plannotator-update Raw
1#!/usr/bin/env bash
2#
3# plannotator-update — update Plannotator, then re-apply the local --gate --json
4# patch that the installer overwrites.
5#
6# The upstream `plannotator-annotate` skill invokes the binary with no flags.
7# Ryan runs `permissions.defaultMode: auto`, so the plugin's plan-review hook
8# never fires and `/plannotator-annotate` is the only path to an Approve button.
9# `--gate` adds that button; `--json` makes Close emit {"decision":"dismissed"}
10# instead of empty stdout and lets approve-with-notes survive. Flags go *before*
11# $ARGUMENTS so nothing the user types can swallow them as a value
12# (--result-file is the only value-taking flag).
13#
14# Usage:
15# plannotator-update # install latest, then patch
16# plannotator-update --patch-only # skip the download, just re-apply the patch
17# plannotator-update --check # report patch state, change nothing
18
19set -euo pipefail
20
21INSTALL_URL="https://plannotator.ai/install.sh"
22SKILL="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/plannotator-annotate/SKILL.md"
23
24UNPATCHED='!`plannotator annotate $ARGUMENTS`'
25PATCHED='!`plannotator annotate --gate --json $ARGUMENTS`'
26
27MODE=install
28case "${1-}" in
29 --patch-only) MODE=patch ;;
30 --check) MODE=check ;;
31 -h|--help) awk 'NR==1{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "$0" ; exit 0 ;;
32 "") ;;
33 *) echo "plannotator-update: unknown option '$1'" >&2 ; exit 2 ;;
34esac
35
36version() { command -v plannotator >/dev/null 2>&1 && plannotator --version 2>/dev/null || echo "(not installed)"; }
37
38# --- update ---------------------------------------------------------------
39if [ "$MODE" = install ]; then
40 before=$(version)
41 echo "==> Updating Plannotator (currently: $before)"
42 # --yes keeps it unattended; the installer still honours the saved answers
43 # in ~/.plannotator/install-prefs (extras=no, model_invocable=none).
44 curl -fsSL "$INSTALL_URL" | bash -s -- --yes
45 after=$(version)
46 if [ "$before" = "$after" ]; then
47 echo "==> Version unchanged: $after"
48 else
49 echo "==> Updated: $before -> $after"
50 fi
51fi
52
53# --- patch ----------------------------------------------------------------
54if [ ! -f "$SKILL" ]; then
55 echo "plannotator-update: skill not found at $SKILL" >&2
56 echo " The installer should have written it. Nothing patched." >&2
57 exit 1
58fi
59
60if grep -qxF "$PATCHED" "$SKILL"; then
61 echo "==> Skill already patched (--gate --json present)"
62 exit 0
63fi
64
65if [ "$MODE" = check ]; then
66 echo "==> Skill is NOT patched. Invocation line(s) found:"
67 grep -nF 'plannotator annotate' "$SKILL" >&2 || echo " (none)" >&2
68 exit 1
69fi
70
71if ! grep -qxF "$UNPATCHED" "$SKILL"; then
72 # Upstream changed the invocation line. Refuse to guess — a wrong patch here
73 # fails silently as a missing Approve button, which is exactly the bug this
74 # script exists to prevent.
75 echo "plannotator-update: cannot patch — expected invocation line not found." >&2
76 echo " expected: $UNPATCHED" >&2
77 echo " found in $SKILL:" >&2
78 grep -nF 'plannotator annotate' "$SKILL" >&2 || echo " (no 'plannotator annotate' line at all)" >&2
79 echo " Upstream likely reshaped the skill. Re-apply --gate --json by hand" >&2
80 echo " and update the OLD/NEW strings in $0." >&2
81 exit 1
82fi
83
84tmp=$(mktemp)
85trap 'rm -f "$tmp"' EXIT
86# Exact whole-line match, so no regex metacharacter can misfire on $ARGUMENTS.
87awk -v old="$UNPATCHED" -v new="$PATCHED" '
88 $0 == old { $0 = new; n++ }
89 { print }
90 END { exit(n ? 0 : 1) }
91' "$SKILL" > "$tmp"
92cat "$tmp" > "$SKILL"
93
94grep -qxF "$PATCHED" "$SKILL" || { echo "plannotator-update: patch verification failed" >&2; exit 1; }
95echo "==> Patched $SKILL"
96echo " $PATCHED"
97
98# Note: ~/.agents/skills/plannotator-annotate is the "core" skill flavour — it has
99# no inline-bash $ARGUMENTS line and is deliberately left alone.
100