Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
.tessl
.artifacts

.update-available

__pycache__/
*.py[cod]

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
148 changes: 148 additions & 0 deletions hack/aiw-update.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Pull latest ai-workflows and refresh installs if command wrappers changed.
#
# Usage:
# 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 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

INSTALL_DIR="${AI_WORKFLOWS_DIR:-${HOME}/.ai-workflows}"
REPO_DIR="$(readlink -f "$INSTALL_DIR")"
CHECKOUT_MAIN=false
FORCE_REINSTALL=false
PROJECTS=()
FORCED_TARGETS=()

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
;;
--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,12p' "$0"
exit 0
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
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
return 0
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
git checkout main
fi

git fetch 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"

CHANGED_COMMANDS=false
if [[ "$BEFORE_HEAD" != "$AFTER_HEAD" ]]; 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

mapfile -t INSTALL_TARGETS < <(detect_install_targets)

if $FORCE_REINSTALL || $CHANGED_COMMANDS; then
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
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))"
64 changes: 64 additions & 0 deletions hack/install-update-timer.sh
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions hack/systemd/ai-workflows-update-check.service
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions hack/systemd/ai-workflows-update-check.timer
Original file line number Diff line number Diff line change
@@ -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
111 changes: 111 additions & 0 deletions hack/update-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/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}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
MARKER="${INSTALL_DIR}/.update-available"
STATE_DIR="${XDG_STATE_HOME:-${HOME}/.local/state}/ai-workflows"
LAST_NOTIFIED="${STATE_DIR}/last-notified-sha"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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"

# Serialize timer + --once paths so concurrent runs cannot double-notify.
exec 9>"${STATE_DIR}/update-check.lock"
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 "$FETCH_REMOTE" "$FETCH_BRANCH" 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)"
UPDATE_CMD="aiw-update"
if [[ "$BRANCH" != "main" ]]; then
UPDATE_CMD="aiw-update --checkout-main"
fi

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" <<EOF
behind=${BEHIND}
remote_ref=${REMOTE_REF}
remote_sha=${REMOTE_SHA}
branch=${BRANCH}
checked_at=$(date -Iseconds)
latest_subject=${SUBJECT}
update_cmd=${UPDATE_CMD}
EOF

echo "ai-workflows: ${BEHIND} commit(s) behind ${REMOTE_REF} (${SHORT_SHA})"

# Deduplicate notifications for the same remote tip.
if [[ -f "$LAST_NOTIFIED" ]] && [[ "$(cat "$LAST_NOTIFIED")" == "$REMOTE_SHA" ]]; then
echo "ai-workflows: already notified for ${SHORT_SHA}; skipping toast"
exit 0
fi

COMMIT_WORD="commits"
[[ "$BEHIND" -eq 1 ]] && COMMIT_WORD="commit"

TITLE="New update for ai-workflows"
BODY="${BEHIND} new ${COMMIT_WORD} on main

Update with:
${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.
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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading