#!/usr/bin/env bash # # plannotator-update — update Plannotator, then re-apply the local --gate --json # patch that the installer overwrites. # # The upstream `plannotator-annotate` skill invokes the binary with no flags. # Ryan runs `permissions.defaultMode: auto`, so the plugin's plan-review hook # never fires and `/plannotator-annotate` is the only path to an Approve button. # `--gate` adds that button; `--json` makes Close emit {"decision":"dismissed"} # instead of empty stdout and lets approve-with-notes survive. Flags go *before* # $ARGUMENTS so nothing the user types can swallow them as a value # (--result-file is the only value-taking flag). # # Usage: # plannotator-update # install latest, then patch # plannotator-update --patch-only # skip the download, just re-apply the patch # plannotator-update --check # report patch state, change nothing set -euo pipefail INSTALL_URL="https://plannotator.ai/install.sh" SKILL="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/plannotator-annotate/SKILL.md" UNPATCHED='!`plannotator annotate $ARGUMENTS`' PATCHED='!`plannotator annotate --gate --json $ARGUMENTS`' MODE=install case "${1-}" in --patch-only) MODE=patch ;; --check) MODE=check ;; -h|--help) awk 'NR==1{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "$0" ; exit 0 ;; "") ;; *) echo "plannotator-update: unknown option '$1'" >&2 ; exit 2 ;; esac version() { command -v plannotator >/dev/null 2>&1 && plannotator --version 2>/dev/null || echo "(not installed)"; } # --- update --------------------------------------------------------------- if [ "$MODE" = install ]; then before=$(version) echo "==> Updating Plannotator (currently: $before)" # --yes keeps it unattended; the installer still honours the saved answers # in ~/.plannotator/install-prefs (extras=no, model_invocable=none). curl -fsSL "$INSTALL_URL" | bash -s -- --yes after=$(version) if [ "$before" = "$after" ]; then echo "==> Version unchanged: $after" else echo "==> Updated: $before -> $after" fi fi # --- patch ---------------------------------------------------------------- if [ ! -f "$SKILL" ]; then echo "plannotator-update: skill not found at $SKILL" >&2 echo " The installer should have written it. Nothing patched." >&2 exit 1 fi if grep -qxF "$PATCHED" "$SKILL"; then echo "==> Skill already patched (--gate --json present)" exit 0 fi if [ "$MODE" = check ]; then echo "==> Skill is NOT patched. Invocation line(s) found:" grep -nF 'plannotator annotate' "$SKILL" >&2 || echo " (none)" >&2 exit 1 fi if ! grep -qxF "$UNPATCHED" "$SKILL"; then # Upstream changed the invocation line. Refuse to guess — a wrong patch here # fails silently as a missing Approve button, which is exactly the bug this # script exists to prevent. echo "plannotator-update: cannot patch — expected invocation line not found." >&2 echo " expected: $UNPATCHED" >&2 echo " found in $SKILL:" >&2 grep -nF 'plannotator annotate' "$SKILL" >&2 || echo " (no 'plannotator annotate' line at all)" >&2 echo " Upstream likely reshaped the skill. Re-apply --gate --json by hand" >&2 echo " and update the OLD/NEW strings in $0." >&2 exit 1 fi tmp=$(mktemp) trap 'rm -f "$tmp"' EXIT # Exact whole-line match, so no regex metacharacter can misfire on $ARGUMENTS. awk -v old="$UNPATCHED" -v new="$PATCHED" ' $0 == old { $0 = new; n++ } { print } END { exit(n ? 0 : 1) } ' "$SKILL" > "$tmp" cat "$tmp" > "$SKILL" grep -qxF "$PATCHED" "$SKILL" || { echo "plannotator-update: patch verification failed" >&2; exit 1; } echo "==> Patched $SKILL" echo " $PATCHED" # Note: ~/.agents/skills/plannotator-annotate is the "core" skill flavour — it has # no inline-bash $ARGUMENTS line and is deliberately left alone.