From b0f0d44966fc6edeab35541bf66bd57a24bbafe5 Mon Sep 17 00:00:00 2001 From: Gal Elharar Date: Thu, 30 Jul 2026 18:50:15 +0300 Subject: [PATCH 1/4] Add optional daily Linux update notifier Prompt during install to enable a systemd user timer that notifies when ~/.ai-workflows falls behind origin/main, with aiw-update to pull and refresh. Co-authored-by: Cursor --- .gitignore | 2 + README.md | 14 +++ hack/aiw-update.sh | 80 ++++++++++++++++ hack/install-update-timer.sh | 64 +++++++++++++ .../systemd/ai-workflows-update-check.service | 12 +++ hack/systemd/ai-workflows-update-check.timer | 12 +++ hack/update-check.sh | 91 +++++++++++++++++++ install.sh | 71 ++++++++++++++- uninstall.sh | 3 + 9 files changed, 348 insertions(+), 1 deletion(-) create mode 100755 hack/aiw-update.sh create mode 100755 hack/install-update-timer.sh create mode 100644 hack/systemd/ai-workflows-update-check.service create mode 100644 hack/systemd/ai-workflows-update-check.timer create mode 100755 hack/update-check.sh diff --git a/.gitignore b/.gitignore index 8b997ef..ac8b241 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ .tessl .artifacts +.update-available + __pycache__/ *.py[cod] diff --git a/README.md b/README.md index 8056e77..b6b5dbd 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,20 @@ Cursor scans both project-level (`.cursor/commands/`) and user-level (`~/.cursor ```bash cd ~/.ai-workflows && git pull +# or, if you enabled the update notifier: +aiw-update +``` + +### Optional: daily update notifier (Linux) + +On Linux desktops with systemd user sessions and `notify-send`, `install.sh` can prompt to enable a daily check. If `~/.ai-workflows` is behind `origin/main`, you get a desktop notification suggesting `aiw-update`. + +```bash +./install.sh cursor # prompts [y/N] when supported +./install.sh cursor --with-update-timer +./install.sh cursor --no-update-timer +./hack/install-update-timer.sh # enable later +./hack/install-update-timer.sh --remove ``` ## Uninstalling diff --git a/hack/aiw-update.sh b/hack/aiw-update.sh new file mode 100755 index 0000000..38aad5a --- /dev/null +++ b/hack/aiw-update.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Pull latest ai-workflows and refresh Cursor installs if command wrappers changed. +# +# Usage: +# aiw-update # ff-only pull origin/main into current branch tip +# aiw-update --checkout-main # checkout main first, then pull +# aiw-update --reinstall # always re-run install.sh (user-level cursor) +# aiw-update --project PATH # also reinstall Cursor skills for PATH (repeatable) + +set -euo pipefail + +INSTALL_DIR="${AI_WORKFLOWS_DIR:-${HOME}/.ai-workflows}" +REPO_DIR="$(readlink -f "$INSTALL_DIR")" +CHECKOUT_MAIN=false +FORCE_REINSTALL=false +PROJECTS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --checkout-main) CHECKOUT_MAIN=true ;; + --reinstall) FORCE_REINSTALL=true ;; + --project) + if [[ -z "${2:-}" || "${2:0:1}" == "-" ]]; then + echo "Error: --project requires a path" >&2 + exit 1 + fi + PROJECTS+=("$2") + shift + ;; + -h|--help) + sed -n '2,10p' "$0" + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac + shift +done + +cd "$REPO_DIR" + +BEFORE_HEAD="$(git rev-parse HEAD)" + +if $CHECKOUT_MAIN; then + git checkout main +fi + +git fetch origin main +git pull --ff-only origin main + +AFTER_HEAD="$(git rev-parse HEAD)" +rm -f "${INSTALL_DIR}/.update-available" + +CHANGED_COMMANDS=false +if [[ "$BEFORE_HEAD" != "$AFTER_HEAD" ]]; then + if git diff --name-only "$BEFORE_HEAD" "$AFTER_HEAD" -- '*/commands/*.md' install.sh uninstall.sh | grep -q .; then + CHANGED_COMMANDS=true + fi +fi + +if $FORCE_REINSTALL || $CHANGED_COMMANDS; then + echo "Refreshing user-level Cursor install..." + # Avoid re-prompting for the update timer on every refresh. + "${REPO_DIR}/install.sh" cursor --no-update-timer + + for p in "${PROJECTS[@]}"; do + if [[ -d "$p" ]]; then + echo "Reinstalling Cursor skills for $p" + "${REPO_DIR}/install.sh" cursor --project "$p" --no-update-timer + else + echo "Warning: project path not found, skipping: $p" >&2 + fi + done +else + echo "Skill content updated via symlink; no command regeneration needed." +fi + +echo "Done. Now at $(git rev-parse --short HEAD) ($(git branch --show-current))" diff --git a/hack/install-update-timer.sh b/hack/install-update-timer.sh new file mode 100755 index 0000000..67337e6 --- /dev/null +++ b/hack/install-update-timer.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Install (or refresh) the systemd --user timer that checks for ai-workflows updates. +# +# Usage: +# ./hack/install-update-timer.sh # enable + start +# ./hack/install-update-timer.sh --remove # disable + remove units +# ./hack/install-update-timer.sh --once # run the check once now + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +UNIT_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user" +BIN_DIR="${HOME}/.local/bin" + +ACTION=install +for arg in "$@"; do + case "$arg" in + --remove) ACTION=remove ;; + --once) ACTION=once ;; + -h|--help) + sed -n '2,10p' "$0" + exit 0 + ;; + esac +done + +chmod +x "${REPO_DIR}/hack/update-check.sh" "${REPO_DIR}/hack/aiw-update.sh" + +case "$ACTION" in + once) + exec "${REPO_DIR}/hack/update-check.sh" + ;; + remove) + systemctl --user disable --now ai-workflows-update-check.timer 2>/dev/null || true + rm -f "${UNIT_DIR}/ai-workflows-update-check.service" \ + "${UNIT_DIR}/ai-workflows-update-check.timer" + systemctl --user daemon-reload + rm -f "${BIN_DIR}/aiw-update" + echo "Removed ai-workflows update timer." + exit 0 + ;; +esac + +mkdir -p "$UNIT_DIR" "$BIN_DIR" +cp "${REPO_DIR}/hack/systemd/ai-workflows-update-check.service" "$UNIT_DIR/" +cp "${REPO_DIR}/hack/systemd/ai-workflows-update-check.timer" "$UNIT_DIR/" + +# Convenience command on PATH +ln -sfn "${REPO_DIR}/hack/aiw-update.sh" "${BIN_DIR}/aiw-update" + +systemctl --user daemon-reload +systemctl --user enable --now ai-workflows-update-check.timer + +echo "Installed ai-workflows update timer (daily, ~00:00)." +echo " status: systemctl --user status ai-workflows-update-check.timer" +echo " logs: journalctl --user -u ai-workflows-update-check.service -f" +echo " update: aiw-update" +echo " remove: ${REPO_DIR}/hack/install-update-timer.sh --remove" +echo +echo "Note: Linger is off — the timer runs while your user session is active." +echo "Persistent=true will catch a missed daily run on next login." +echo +echo "Running an initial check now (notification only if you are behind main)..." +"${REPO_DIR}/hack/update-check.sh" || true diff --git a/hack/systemd/ai-workflows-update-check.service b/hack/systemd/ai-workflows-update-check.service new file mode 100644 index 0000000..1728926 --- /dev/null +++ b/hack/systemd/ai-workflows-update-check.service @@ -0,0 +1,12 @@ +[Unit] +Description=Check for ai-workflows updates +Documentation=file://%h/.ai-workflows/hack/update-check.sh +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=%h/.ai-workflows/hack/update-check.sh +# Soften failure so a flaky network never marks the timer failed permanently. +SuccessExitStatus=0 +Environment=AI_WORKFLOWS_DIR=%h/.ai-workflows diff --git a/hack/systemd/ai-workflows-update-check.timer b/hack/systemd/ai-workflows-update-check.timer new file mode 100644 index 0000000..7bb4b77 --- /dev/null +++ b/hack/systemd/ai-workflows-update-check.timer @@ -0,0 +1,12 @@ +[Unit] +Description=Periodic ai-workflows update check +Documentation=file://%h/.ai-workflows/hack/update-check.sh + +[Timer] +OnCalendar=daily +Persistent=true +AccuracySec=1h +Unit=ai-workflows-update-check.service + +[Install] +WantedBy=timers.target diff --git a/hack/update-check.sh b/hack/update-check.sh new file mode 100755 index 0000000..87894f2 --- /dev/null +++ b/hack/update-check.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Check whether the local ai-workflows clone is behind origin/main. +# If so, write a marker file and send a desktop notification (Fedora/Linux). +# +# Intended to run from a systemd --user timer. Safe to run repeatedly: +# notifies at most once per new origin/main SHA. + +set -euo pipefail + +INSTALL_DIR="${AI_WORKFLOWS_DIR:-${HOME}/.ai-workflows}" +REMOTE_REF="${AI_WORKFLOWS_REMOTE_REF:-origin/main}" +MARKER="${INSTALL_DIR}/.update-available" +STATE_DIR="${XDG_STATE_HOME:-${HOME}/.local/state}/ai-workflows" +LAST_NOTIFIED="${STATE_DIR}/last-notified-sha" + +if [[ ! -d "$INSTALL_DIR" ]]; then + echo "ai-workflows: install dir not found: $INSTALL_DIR" >&2 + exit 0 +fi + +# Resolve through symlink so we operate on the real git clone. +REPO_DIR="$(readlink -f "$INSTALL_DIR")" +if [[ ! -d "${REPO_DIR}/.git" ]] && ! git -C "$REPO_DIR" rev-parse --git-dir >/dev/null 2>&1; then + echo "ai-workflows: not a git repo: $REPO_DIR" >&2 + exit 0 +fi + +mkdir -p "$STATE_DIR" + +cd "$REPO_DIR" + +# Fetch quietly; network failures should not spam the user. +if ! git fetch --quiet origin main 2>/dev/null; then + echo "ai-workflows: fetch failed (offline?); skipping check" + exit 0 +fi + +if ! git rev-parse --verify "$REMOTE_REF" >/dev/null 2>&1; then + echo "ai-workflows: missing ref $REMOTE_REF" >&2 + exit 0 +fi + +REMOTE_SHA="$(git rev-parse "$REMOTE_REF")" +BEHIND="$(git rev-list --count "HEAD..${REMOTE_REF}" 2>/dev/null || echo 0)" +BRANCH="$(git branch --show-current 2>/dev/null || echo detached)" + +if [[ "$BEHIND" -eq 0 ]]; then + rm -f "$MARKER" + echo "ai-workflows: up to date with ${REMOTE_REF} (branch=${BRANCH})" + exit 0 +fi + +SHORT_SHA="$(git rev-parse --short "$REMOTE_SHA")" +SUBJECT="$(git log -1 --format='%s' "$REMOTE_SHA")" + +cat > "$MARKER" </dev/null 2>&1; then + # --expire-time in ms; keep it visible long enough to notice during testing. + notify-send --app-name="ai-workflows" --urgency=normal --expire-time=20000 \ + "$TITLE" "$BODY" || true +else + echo "ai-workflows: notify-send not found; wrote marker at $MARKER" >&2 +fi + +echo "$REMOTE_SHA" > "$LAST_NOTIFIED" diff --git a/install.sh b/install.sh index 8bba840..bcdc025 100755 --- a/install.sh +++ b/install.sh @@ -18,11 +18,14 @@ # ./install.sh all # user-level Cursor + Claude + Gemini # ./install.sh all --project [path] # project-level Cursor + Claude + Gemini # ./install.sh --list # list available workflows +# ./install.sh cursor --with-update-timer # also enable daily update notifier +# ./install.sh cursor --no-update-timer # skip the update-notifier prompt set -e REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" INSTALL_DIR="${HOME}/.ai-workflows" +UPDATE_TIMER="ask" # ask | yes | no # --- discover all available workflows --- ALL_WORKFLOWS=() @@ -68,6 +71,12 @@ while [[ $# -gt 0 ]]; do exit 1 fi ;; + --with-update-timer) + UPDATE_TIMER="yes" + ;; + --no-update-timer) + UPDATE_TIMER="no" + ;; esac shift done @@ -285,6 +294,63 @@ install_gemini() { done } +# Offer a daily systemd --user notifier (Linux desktop). Default: no. +maybe_offer_update_timer() { + local installer="${REPO_DIR}/hack/install-update-timer.sh" + + if [[ "$UPDATE_TIMER" == "no" ]]; then + return + fi + if [[ ! -x "$installer" ]]; then + return + fi + # Linux + desktop notification stack only. + if [[ "$(uname -s)" != "Linux" ]]; then + return + fi + if ! command -v systemctl >/dev/null 2>&1; then + return + fi + if ! systemctl --user status >/dev/null 2>&1; then + return + fi + if ! command -v notify-send >/dev/null 2>&1; then + return + fi + # Already enabled — don't re-prompt on every install. + if systemctl --user is-enabled ai-workflows-update-check.timer >/dev/null 2>&1; then + echo "Update notifier already enabled (ai-workflows-update-check.timer)." + return + fi + + local answer="n" + if [[ "$UPDATE_TIMER" == "yes" ]]; then + answer="y" + elif [[ -t 0 || -r /dev/tty ]]; then + echo + echo "Optional: enable a daily desktop notification when ai-workflows is behind main?" + echo " (Linux/systemd; run 'aiw-update' when notified. Default: No)" + if [[ -t 0 ]]; then + read -r -p "Enable daily update notifier? [y/N] " answer || true + else + # stdin may be piped; still prompt on the real terminal when available. + read -r -p "Enable daily update notifier? [y/N] " answer &2 echo " --project [path] project-level (.cursor/skills/, .claude/, .gemini/skills/)" >&2 echo " path defaults to current directory" >&2 + echo " --with-update-timer enable daily Linux update notifier (no prompt)" >&2 + echo " --no-update-timer skip the update-notifier prompt" >&2 echo " --list list available workflows and exit" >&2 exit 1 ;; esac -echo "Done. Run 'git pull' from $INSTALL_DIR to update." \ No newline at end of file +echo "Done. Run 'git pull' from $INSTALL_DIR to update (or: aiw-update)." +maybe_offer_update_timer \ No newline at end of file diff --git a/uninstall.sh b/uninstall.sh index 0921f9f..00139b7 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -264,6 +264,9 @@ case "$TARGET" in uninstall_claude uninstall_gemini if [[ "$SCOPE" == "user" && "$SELECTIVE" == false ]]; then + if [[ -x "${REPO_DIR}/hack/install-update-timer.sh" ]]; then + "${REPO_DIR}/hack/install-update-timer.sh" --remove >/dev/null 2>&1 || true + fi uninstall_link fi ;; From e2cce957154c3d0dd68fccf47c8297319e642c24 Mon Sep 17 00:00:00 2001 From: Gal Elharar Date: Thu, 30 Jul 2026 19:01:39 +0300 Subject: [PATCH 2/4] Address CodeRabbit feedback on update notifier Refresh Cursor installs on SKILL.md changes, only dedupe successful toasts, surface explicit --with-update-timer skip reasons, keep install success if the optional timer setup fails, advertise aiw-update only when present, and fail uninstall if timer removal fails. Co-authored-by: Cursor --- hack/aiw-update.sh | 2 +- hack/update-check.sh | 12 ++++++++---- install.sh | 33 ++++++++++++++++++++++----------- uninstall.sh | 5 ++++- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/hack/aiw-update.sh b/hack/aiw-update.sh index 38aad5a..380cea0 100755 --- a/hack/aiw-update.sh +++ b/hack/aiw-update.sh @@ -55,7 +55,7 @@ rm -f "${INSTALL_DIR}/.update-available" CHANGED_COMMANDS=false if [[ "$BEFORE_HEAD" != "$AFTER_HEAD" ]]; then - if git diff --name-only "$BEFORE_HEAD" "$AFTER_HEAD" -- '*/commands/*.md' install.sh uninstall.sh | grep -q .; then + if git diff --name-only "$BEFORE_HEAD" "$AFTER_HEAD" -- '*/commands/*.md' '*/SKILL.md' install.sh uninstall.sh | grep -q .; then CHANGED_COMMANDS=true fi fi diff --git a/hack/update-check.sh b/hack/update-check.sh index 87894f2..ceffd63 100755 --- a/hack/update-check.sh +++ b/hack/update-check.sh @@ -82,10 +82,14 @@ Update with: if command -v notify-send >/dev/null 2>&1; then # --expire-time in ms; keep it visible long enough to notice during testing. - notify-send --app-name="ai-workflows" --urgency=normal --expire-time=20000 \ - "$TITLE" "$BODY" || true + if notify-send --app-name="ai-workflows" --urgency=normal --expire-time=20000 \ + "$TITLE" "$BODY"; then + echo "$REMOTE_SHA" > "$LAST_NOTIFIED" + else + echo "ai-workflows: notify-send failed; will retry next check" >&2 + fi else + # No desktop notifier — keep the marker file and avoid spamming every run. echo "ai-workflows: notify-send not found; wrote marker at $MARKER" >&2 + echo "$REMOTE_SHA" > "$LAST_NOTIFIED" fi - -echo "$REMOTE_SHA" > "$LAST_NOTIFIED" diff --git a/install.sh b/install.sh index bcdc025..cbd549a 100755 --- a/install.sh +++ b/install.sh @@ -299,28 +299,33 @@ maybe_offer_update_timer() { local installer="${REPO_DIR}/hack/install-update-timer.sh" if [[ "$UPDATE_TIMER" == "no" ]]; then - return + return 0 fi if [[ ! -x "$installer" ]]; then - return + [[ "$UPDATE_TIMER" == "yes" ]] && echo "Update notifier: installer script missing/not executable; skipping." >&2 + return 0 fi # Linux + desktop notification stack only. if [[ "$(uname -s)" != "Linux" ]]; then - return + [[ "$UPDATE_TIMER" == "yes" ]] && echo "Update notifier requires Linux; --with-update-timer ignored." >&2 + return 0 fi if ! command -v systemctl >/dev/null 2>&1; then - return + [[ "$UPDATE_TIMER" == "yes" ]] && echo "Update notifier requires systemd; --with-update-timer ignored." >&2 + return 0 fi if ! systemctl --user status >/dev/null 2>&1; then - return + [[ "$UPDATE_TIMER" == "yes" ]] && echo "Update notifier requires an active systemd --user session; --with-update-timer ignored." >&2 + return 0 fi if ! command -v notify-send >/dev/null 2>&1; then - return + [[ "$UPDATE_TIMER" == "yes" ]] && echo "Update notifier requires notify-send; --with-update-timer ignored." >&2 + return 0 fi # Already enabled — don't re-prompt on every install. if systemctl --user is-enabled ai-workflows-update-check.timer >/dev/null 2>&1; then echo "Update notifier already enabled (ai-workflows-update-check.timer)." - return + return 0 fi local answer="n" @@ -338,12 +343,14 @@ maybe_offer_update_timer() { fi else # Non-interactive: skip unless --with-update-timer was passed. - return + return 0 fi case "${answer,,}" in y|yes) - "$installer" + if ! "$installer"; then + echo "Warning: failed to enable update notifier; retry later with: ${installer}" >&2 + fi ;; *) echo "Skipped update notifier. Enable later with: ${installer}" @@ -393,5 +400,9 @@ case "$TARGET" in ;; esac -echo "Done. Run 'git pull' from $INSTALL_DIR to update (or: aiw-update)." -maybe_offer_update_timer \ No newline at end of file +maybe_offer_update_timer +if command -v aiw-update >/dev/null 2>&1 || [[ -x "${HOME}/.local/bin/aiw-update" ]]; then + echo "Done. Run 'git pull' from $INSTALL_DIR to update (or: aiw-update)." +else + echo "Done. Run 'git pull' from $INSTALL_DIR to update." +fi \ No newline at end of file diff --git a/uninstall.sh b/uninstall.sh index 00139b7..6e074cd 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -265,7 +265,10 @@ case "$TARGET" in uninstall_gemini if [[ "$SCOPE" == "user" && "$SELECTIVE" == false ]]; then if [[ -x "${REPO_DIR}/hack/install-update-timer.sh" ]]; then - "${REPO_DIR}/hack/install-update-timer.sh" --remove >/dev/null 2>&1 || true + if ! "${REPO_DIR}/hack/install-update-timer.sh" --remove; then + echo "Error: failed to remove update notifier via ${REPO_DIR}/hack/install-update-timer.sh --remove" >&2 + exit 1 + fi fi uninstall_link fi From e9e734760fa8814af58757cc411871bbc99e4eba Mon Sep 17 00:00:00 2001 From: Gal Elharar Date: Sun, 9 Aug 2026 11:25:08 +0300 Subject: [PATCH 3/4] Address Andy and CodeRabbit review on update notifier Detect installed Cursor/Claude/Gemini targets in aiw-update, require main (or --checkout-main) before pull, flock update-check, warn-and-continue on optional timer uninstall failure, and only advertise aiw-update when on PATH. Co-authored-by: Cursor --- hack/aiw-update.sh | 80 +++++++++++++++++++++++++++++++++++++++----- hack/update-check.sh | 4 +++ install.sh | 6 ++-- uninstall.sh | 3 +- 4 files changed, 81 insertions(+), 12 deletions(-) diff --git a/hack/aiw-update.sh b/hack/aiw-update.sh index 380cea0..bc01341 100755 --- a/hack/aiw-update.sh +++ b/hack/aiw-update.sh @@ -1,11 +1,13 @@ #!/usr/bin/env bash -# Pull latest ai-workflows and refresh Cursor installs if command wrappers changed. +# Pull latest ai-workflows and refresh installs if command wrappers changed. # # Usage: -# aiw-update # ff-only pull origin/main into current branch tip +# aiw-update # ff-only pull origin/main (must be on main) # aiw-update --checkout-main # checkout main first, then pull -# aiw-update --reinstall # always re-run install.sh (user-level cursor) +# aiw-update --reinstall # always re-run install.sh for detected targets # aiw-update --project PATH # also reinstall Cursor skills for PATH (repeatable) +# aiw-update --target NAME # force install target(s): cursor|claude|gemini|all +# # (repeatable; default: auto-detect from ~/.cursor etc.) set -euo pipefail @@ -14,6 +16,7 @@ REPO_DIR="$(readlink -f "$INSTALL_DIR")" CHECKOUT_MAIN=false FORCE_REINSTALL=false PROJECTS=() +FORCED_TARGETS=() while [[ $# -gt 0 ]]; do case "$1" in @@ -27,8 +30,16 @@ while [[ $# -gt 0 ]]; do PROJECTS+=("$2") shift ;; + --target) + if [[ -z "${2:-}" || "${2:0:1}" == "-" ]]; then + echo "Error: --target requires cursor|claude|gemini|all" >&2 + exit 1 + fi + FORCED_TARGETS+=("$2") + shift + ;; -h|--help) - sed -n '2,10p' "$0" + sed -n '2,12p' "$0" exit 0 ;; *) @@ -39,8 +50,52 @@ while [[ $# -gt 0 ]]; do shift done +detect_install_targets() { + local targets=() + if [[ ${#FORCED_TARGETS[@]} -gt 0 ]]; then + local t + for t in "${FORCED_TARGETS[@]}"; do + case "$t" in + cursor|claude|gemini|all) targets+=("$t") ;; + *) + echo "Error: unknown --target '$t' (cursor|claude|gemini|all)" >&2 + exit 1 + ;; + esac + done + printf '%s\n' "${targets[@]}" + return + fi + + if [[ -d "${HOME}/.cursor/skills" ]] || [[ -d "${HOME}/.cursor/commands" ]]; then + targets+=(cursor) + fi + if [[ -f "${HOME}/.claude/CLAUDE.md" ]] && grep -qF '# ai-workflows' "${HOME}/.claude/CLAUDE.md" 2>/dev/null; then + targets+=(claude) + elif [[ -d "${HOME}/.claude/skills" ]]; then + # Skills dir present — refresh even if marker line was customized. + targets+=(claude) + fi + if [[ -d "${HOME}/.gemini/skills" ]]; then + targets+=(gemini) + fi + + if [[ ${#targets[@]} -eq 0 ]]; then + targets+=(cursor) + fi + printf '%s\n' "${targets[@]}" +} + cd "$REPO_DIR" +CURRENT_BRANCH="$(git branch --show-current 2>/dev/null || true)" +if [[ "$CURRENT_BRANCH" != "main" ]] && ! $CHECKOUT_MAIN; then + echo "ai-workflows: not on main (branch=${CURRENT_BRANCH:-detached})." >&2 + echo " Use: aiw-update --checkout-main" >&2 + echo " Or rebase/merge origin/main into this branch, then retry." >&2 + exit 1 +fi + BEFORE_HEAD="$(git rev-parse HEAD)" if $CHECKOUT_MAIN; then @@ -48,7 +103,11 @@ if $CHECKOUT_MAIN; then fi git fetch origin main -git pull --ff-only origin main +if ! git pull --ff-only origin main; then + echo "ai-workflows: fast-forward pull from origin/main failed." >&2 + echo " Resolve local commits on main, or: git reset --hard origin/main (destructive)." >&2 + exit 1 +fi AFTER_HEAD="$(git rev-parse HEAD)" rm -f "${INSTALL_DIR}/.update-available" @@ -60,10 +119,15 @@ if [[ "$BEFORE_HEAD" != "$AFTER_HEAD" ]]; then fi fi +mapfile -t INSTALL_TARGETS < <(detect_install_targets) + if $FORCE_REINSTALL || $CHANGED_COMMANDS; then - echo "Refreshing user-level Cursor install..." - # Avoid re-prompting for the update timer on every refresh. - "${REPO_DIR}/install.sh" cursor --no-update-timer + local_target="" + for local_target in "${INSTALL_TARGETS[@]}"; do + echo "Refreshing user-level install (${local_target})..." + # Avoid re-prompting for the update timer on every refresh. + "${REPO_DIR}/install.sh" "$local_target" --no-update-timer + done for p in "${PROJECTS[@]}"; do if [[ -d "$p" ]]; then diff --git a/hack/update-check.sh b/hack/update-check.sh index ceffd63..037e7f9 100755 --- a/hack/update-check.sh +++ b/hack/update-check.sh @@ -27,6 +27,10 @@ fi mkdir -p "$STATE_DIR" +# Serialize timer + --once paths so concurrent runs cannot double-notify. +exec 9>"${STATE_DIR}/update-check.lock" +flock 9 + cd "$REPO_DIR" # Fetch quietly; network failures should not spam the user. diff --git a/install.sh b/install.sh index cbd549a..a056db2 100755 --- a/install.sh +++ b/install.sh @@ -401,8 +401,10 @@ case "$TARGET" in esac maybe_offer_update_timer -if command -v aiw-update >/dev/null 2>&1 || [[ -x "${HOME}/.local/bin/aiw-update" ]]; then +if command -v aiw-update >/dev/null 2>&1; then echo "Done. Run 'git pull' from $INSTALL_DIR to update (or: aiw-update)." +elif [[ -x "${HOME}/.local/bin/aiw-update" ]]; then + echo "Done. Run 'git pull' from $INSTALL_DIR to update (or: ${HOME}/.local/bin/aiw-update)." else echo "Done. Run 'git pull' from $INSTALL_DIR to update." -fi \ No newline at end of file +fi diff --git a/uninstall.sh b/uninstall.sh index 6e074cd..30975d5 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -266,8 +266,7 @@ case "$TARGET" in if [[ "$SCOPE" == "user" && "$SELECTIVE" == false ]]; then if [[ -x "${REPO_DIR}/hack/install-update-timer.sh" ]]; then if ! "${REPO_DIR}/hack/install-update-timer.sh" --remove; then - echo "Error: failed to remove update notifier via ${REPO_DIR}/hack/install-update-timer.sh --remove" >&2 - exit 1 + echo "Warning: failed to remove update notifier; you may need to clean it up manually" >&2 fi fi uninstall_link From fa45de92273e6b110631ed75a4b6b07e0d4c8d22 Mon Sep 17 00:00:00 2001 From: Gal Elharar Date: Sun, 9 Aug 2026 11:35:15 +0300 Subject: [PATCH 4/4] Tighten update-notifier detection and notification UX Skip Cursor refresh when no IDE install is detected, fetch the configured remote ref, suggest --checkout-main off main, and only prompt when a controlling tty is actually usable. Co-authored-by: Cursor --- hack/aiw-update.sh | 18 +++++++++++------- hack/update-check.sh | 18 +++++++++++++++--- install.sh | 15 ++++++++------- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/hack/aiw-update.sh b/hack/aiw-update.sh index bc01341..b42dbb7 100755 --- a/hack/aiw-update.sh +++ b/hack/aiw-update.sh @@ -81,7 +81,7 @@ detect_install_targets() { fi if [[ ${#targets[@]} -eq 0 ]]; then - targets+=(cursor) + return 0 fi printf '%s\n' "${targets[@]}" } @@ -122,12 +122,16 @@ fi mapfile -t INSTALL_TARGETS < <(detect_install_targets) if $FORCE_REINSTALL || $CHANGED_COMMANDS; then - local_target="" - for local_target in "${INSTALL_TARGETS[@]}"; do - echo "Refreshing user-level install (${local_target})..." - # Avoid re-prompting for the update timer on every refresh. - "${REPO_DIR}/install.sh" "$local_target" --no-update-timer - done + if [[ ${#INSTALL_TARGETS[@]} -eq 0 ]]; then + echo "No Cursor/Claude/Gemini install detected; skipping user-level refresh." + echo " Pass --target cursor|claude|gemini|all to force." + else + for tgt in "${INSTALL_TARGETS[@]}"; do + echo "Refreshing user-level install (${tgt})..." + # Avoid re-prompting for the update timer on every refresh. + "${REPO_DIR}/install.sh" "$tgt" --no-update-timer + done + fi for p in "${PROJECTS[@]}"; do if [[ -d "$p" ]]; then diff --git a/hack/update-check.sh b/hack/update-check.sh index 037e7f9..a69dd7d 100755 --- a/hack/update-check.sh +++ b/hack/update-check.sh @@ -33,8 +33,16 @@ flock 9 cd "$REPO_DIR" +# Fetch the same ref we compare against (default origin/main). +FETCH_REMOTE="${REMOTE_REF%%/*}" +FETCH_BRANCH="${REMOTE_REF#*/}" +if [[ "$FETCH_REMOTE" == "$REMOTE_REF" ]]; then + FETCH_REMOTE="origin" + FETCH_BRANCH="$REMOTE_REF" +fi + # Fetch quietly; network failures should not spam the user. -if ! git fetch --quiet origin main 2>/dev/null; then +if ! git fetch --quiet "$FETCH_REMOTE" "$FETCH_BRANCH" 2>/dev/null; then echo "ai-workflows: fetch failed (offline?); skipping check" exit 0 fi @@ -47,6 +55,10 @@ fi REMOTE_SHA="$(git rev-parse "$REMOTE_REF")" BEHIND="$(git rev-list --count "HEAD..${REMOTE_REF}" 2>/dev/null || echo 0)" BRANCH="$(git branch --show-current 2>/dev/null || echo detached)" +UPDATE_CMD="aiw-update" +if [[ "$BRANCH" != "main" ]]; then + UPDATE_CMD="aiw-update --checkout-main" +fi if [[ "$BEHIND" -eq 0 ]]; then rm -f "$MARKER" @@ -64,7 +76,7 @@ remote_sha=${REMOTE_SHA} branch=${BRANCH} checked_at=$(date -Iseconds) latest_subject=${SUBJECT} -update_cmd=aiw-update +update_cmd=${UPDATE_CMD} EOF echo "ai-workflows: ${BEHIND} commit(s) behind ${REMOTE_REF} (${SHORT_SHA})" @@ -82,7 +94,7 @@ TITLE="New update for ai-workflows" BODY="${BEHIND} new ${COMMIT_WORD} on main Update with: - aiw-update" + ${UPDATE_CMD}" if command -v notify-send >/dev/null 2>&1; then # --expire-time in ms; keep it visible long enough to notice during testing. diff --git a/install.sh b/install.sh index a056db2..1bc2c95 100755 --- a/install.sh +++ b/install.sh @@ -331,16 +331,17 @@ maybe_offer_update_timer() { local answer="n" if [[ "$UPDATE_TIMER" == "yes" ]]; then answer="y" - elif [[ -t 0 || -r /dev/tty ]]; then + elif [[ -t 0 ]]; then echo echo "Optional: enable a daily desktop notification when ai-workflows is behind main?" echo " (Linux/systemd; run 'aiw-update' when notified. Default: No)" - if [[ -t 0 ]]; then - read -r -p "Enable daily update notifier? [y/N] " answer || true - else - # stdin may be piped; still prompt on the real terminal when available. - read -r -p "Enable daily update notifier? [y/N] " answer /dev/tty; } 2>/dev/null; then + # stdin may be piped; prompt on the controlling terminal when available. + echo + echo "Optional: enable a daily desktop notification when ai-workflows is behind main?" + echo " (Linux/systemd; run 'aiw-update' when notified. Default: No)" + read -r -p "Enable daily update notifier? [y/N] " answer