Last active
July 10, 2026 09:28
-
-
Save vr000m/17f39d5fc0cfb10fcb95f8e116370d77 to your computer and use it in GitHub Desktop.
Generate Claude Code spinnerVerbs from recent repo activity (changelogs, releases, merges)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Generate Claude Code spinner verbs and merge them into ~/.claude/settings.json | |
| # as spinnerVerbs. Mode defaults to "append" (mixed into the built-in pool); | |
| # set `mode: replace` in the config or SPINNER_MODE=replace to show only these. | |
| # | |
| # Two kinds of output verbs: | |
| # 1. Plain verbs from the config's `verb:` lines (always appended; they also | |
| # replace the built-in rotation pool used to prefix repo entries). | |
| # 2. Decorated entries derived from recent repo activity, one per item: | |
| # <verb> [repo][date]: "title" | |
| # | |
| # Sources come from a config file (default ~/.config/spinner-verbs.conf), | |
| # one per line, # comments allowed: | |
| # verb: Vibing custom pool verb | |
| # /Users/me/Code/myrepo local clone | |
| # ~/Code/other-repo local clone | |
| # pipecat-ai/pipecat GitHub slug (remote) | |
| # https://github.com/owner/repo GitHub URL (remote) | |
| # https://github.com/owner/repo/blob/main/docs/NEWS.md exact changelog file | |
| # | |
| # Per-source fallback chain (each step only if the previous yields nothing | |
| # within the recency window): | |
| # changelog -> releases/tags -> merges to default branch -> latest commit | |
| # | |
| # Remote sources are fetched from GitHub (default branch, common CHANGELOG | |
| # names probed) so stale local clones don't matter. Non-GitHub remotes are | |
| # not supported — clone them locally instead. | |
| # | |
| # No config file means no sources: nothing runs implicitly. | |
| # | |
| # Entries are filtered to the last $SPINNER_MAX_AGE_DAYS (7) days, capped at | |
| # $SPINNER_MAX_PER_REPO (5) per repo. | |
| set -euo pipefail | |
| CONFIG="${SPINNER_CONFIG:-$HOME/.config/spinner-verbs.conf}" | |
| SETTINGS="${SPINNER_SETTINGS:-$HOME/.claude/settings.json}" | |
| MARKER="${SPINNER_MARKER:-$HOME/.claude/.spinner-verbs-last-run}" | |
| LOCKDIR="${SPINNER_LOCKDIR:-$HOME/.claude/.spinner-verbs.lock}" | |
| LOG="${SPINNER_LOG:-$HOME/.claude/spinner-verbs.log}" | |
| MAX_TITLE_LEN=48 | |
| MAX_AGE_DAYS="${SPINNER_MAX_AGE_DAYS:-7}" | |
| MAX_PER_REPO="${SPINNER_MAX_PER_REPO:-5}" | |
| MAX_PARALLEL="${SPINNER_MAX_PARALLEL:-3}" | |
| BG_TIMEOUT_SECS="${SPINNER_BG_TIMEOUT_SECS:-300}" | |
| # Regeneration involves network calls per remote source (changelog probe, | |
| # releases, PR search, commits) and can take longer than a SessionStart hook | |
| # budget allows. So: skip entirely if a run finished within TTL_HOURS, and | |
| # otherwise fork the real work into the background and return immediately — | |
| # the hook itself never blocks on network I/O. MARKER is only touched by a | |
| # background worker on confirmed success (see main()), never up front, so a | |
| # failed run doesn't get mistaken for a fresh one. | |
| TTL_HOURS="${SPINNER_TTL_HOURS:-6}" | |
| marker_fresh() { | |
| [[ -f "$MARKER" ]] || return 1 | |
| local mtime now age_hours | |
| case "$(uname -s)" in | |
| Darwin) mtime="$(stat -f %m "$MARKER")" ;; | |
| *) mtime="$(stat -c %Y "$MARKER")" ;; | |
| esac | |
| now="$(date +%s)" | |
| age_hours=$(( (now - mtime) / 3600 )) | |
| # A future mtime (clock skew, NTP correction, restored-from-backup marker) | |
| # would otherwise read as fresh forever since age_hours goes negative. | |
| [[ "$age_hours" -ge 0 && "$age_hours" -lt "$TTL_HOURS" ]] | |
| } | |
| cutoff_date() { | |
| case "$(uname -s)" in | |
| Darwin) date -v-"${MAX_AGE_DAYS}"d +%F ;; | |
| *) date -d "${MAX_AGE_DAYS} days ago" +%F ;; | |
| esac | |
| } | |
| # Built-in rotation pool; replaced wholesale by `verb:` config lines. | |
| VERBS=(Shipping Polishing Brewing Refining Tinkering Untangling Rebasing Greasing) | |
| # Global (not `local` to main) so the EXIT trap below can reference them | |
| # safely under `set -u` regardless of which function is on the stack when it | |
| # fires — a trap runs in the caller's scope, where a `local` var of a | |
| # function that already returned would be unbound. | |
| tmp="" | |
| tmpdir="" | |
| # Single trap, set once, for every exit path in the script (normal return, | |
| # early `exit`, or a signal). Setting it more than once would silently | |
| # replace the earlier registration rather than combining them, which is how | |
| # the original tmp-only trap and a later lock-cleanup trap would have ended | |
| # up dropping one or the other's cleanup. | |
| cleanup_on_exit() { | |
| rm -f "$tmp" 2>/dev/null | |
| rm -rf "$tmpdir" 2>/dev/null | |
| [[ "${SPINNER_BG_WORKER:-}" == "1" ]] && rmdir "$LOCKDIR" 2>/dev/null | |
| return 0 | |
| } | |
| trap cleanup_on_exit EXIT INT TERM | |
| pick_verb() { | |
| local name="$1" sum | |
| sum="$(printf '%s' "$name" | cksum | cut -d' ' -f1)" | |
| echo "${VERBS[$((sum % ${#VERBS[@]}))]}" | |
| } | |
| gh_api() { | |
| local path="$1" filter="$2" | |
| if command -v gh >/dev/null; then | |
| gh api "$path" --jq "$filter" 2>/dev/null | |
| else | |
| curl -fsSL --max-time 10 "https://api.github.com/$path" 2>/dev/null | jq -r "$filter" | |
| fi | |
| } | |
| # Third-party titles land in the UI verbatim. tr -d '[:cntrl:]' is | |
| # byte-oriented and misses multi-byte Unicode spoofing characters, so filter | |
| # by codepoint with jq instead: C0/C1 controls + DEL, the Arabic letter mark | |
| # and bidi controls (U+061C, U+200E/F, U+202A–202E, U+2066–2069), zero-width | |
| # and joiner characters (U+200B–200D, U+2060, U+FEFF), and tag characters | |
| # (U+E0000–E007F). jq has no hex literals, hence the decimal codepoints. | |
| sanitize_title() { | |
| jq -Rr '[explode[] | select( | |
| . >= 32 and (. < 127 or . > 159) | |
| and . != 1564 | |
| and (. < 8203 or . > 8207) | |
| and (. < 8234 or . > 8238) | |
| and . != 8288 | |
| and (. < 8294 or . > 8297) | |
| and . != 65279 | |
| and (. < 917504 or . > 917631) | |
| )] | implode' | |
| } | |
| # Parse a changelog file: first "##" heading, falling back to the first | |
| # bullet under it when the heading is just "Unreleased" or a version number. | |
| # Prints "date|title" (date may be empty); prints nothing if unparseable. | |
| parse_changelog() { | |
| local file="$1" heading date title bullet | |
| heading="$(grep -m1 -E '^##+ ' "$file" | sed -E 's/^#+ +//')" || true | |
| [[ -z "$heading" ]] && return 0 | |
| date="$(grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' <<<"$heading" | head -1)" | |
| title="$(sed -E 's/ ?-? ?\[?[0-9]{4}-[0-9]{2}-[0-9]{2}\]?//; s/^\[|\]$//g' <<<"$heading")" | |
| if [[ "$title" =~ ^(Unreleased|v?[0-9][0-9.]*)$ ]]; then | |
| bullet="$(awk '/^## /{n++} n==1 && /^[-*] /{sub(/^[-*] +/,""); gsub(/\*\*/,""); print; exit}' "$file")" | |
| [[ -n "$bullet" ]] && title="$bullet" | |
| fi | |
| printf '%s|%s\n' "$date" "$title" | |
| } | |
| # Merge-commit subjects are boilerplate ("Merge pull request #N from x/y"); | |
| # the PR title usually lives in the body's first line. | |
| merge_title() { | |
| local subject="$1" body="$2" | |
| if [[ "$subject" == "Merge pull request #"* || "$subject" == "Merge branch "* ]] && [[ -n "$body" ]]; then | |
| printf '%s\n' "$body" | |
| else | |
| printf '%s\n' "$subject" | |
| fi | |
| } | |
| # --- Local clone ------------------------------------------------------------- | |
| # Each entry_* helper prints zero or more "name|date|title" lines. | |
| local_default_branch() { | |
| local repo="$1" ref | |
| ref="$(git -C "$repo" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)" || true | |
| if [[ -n "$ref" ]]; then | |
| echo "${ref#origin/}" | |
| elif git -C "$repo" show-ref --verify --quiet refs/heads/main; then | |
| echo main | |
| else | |
| echo master | |
| fi | |
| } | |
| entry_local() { | |
| local repo="$1" cutoff="$2" name changelog entry date title | |
| name="$(basename "$repo")" | |
| # 1. Changelog | |
| changelog="$(find "$repo" -maxdepth 1 -iname 'CHANGELOG*' -type f | head -1)" | |
| if [[ -n "$changelog" ]]; then | |
| entry="$(parse_changelog "$changelog")" | |
| if [[ -n "$entry" ]]; then | |
| date="${entry%%|*}"; title="${entry#*|}" | |
| [[ -z "$date" ]] && date="$(git -C "$repo" log -1 --format=%as -- "$changelog" 2>/dev/null || true)" | |
| if [[ -n "$date" && ! "$date" < "$cutoff" ]]; then | |
| printf '%s|%s|%s\n' "$name" "$date" "$title" | |
| return 0 | |
| fi | |
| fi | |
| fi | |
| # 2. Releases (tags) | |
| entry="$(git -C "$repo" tag --sort=-creatordate --format='%(creatordate:short)|%(refname:short)' 2>/dev/null | head -1)" || true | |
| if [[ -n "$entry" ]]; then | |
| date="${entry%%|*}"; title="released ${entry#*|}" | |
| if [[ -n "$date" && ! "$date" < "$cutoff" ]]; then | |
| printf '%s|%s|%s\n' "$name" "$date" "$title" | |
| return 0 | |
| fi | |
| fi | |
| # 3. Merges to default branch within the window | |
| local branch merged | |
| branch="$(local_default_branch "$repo")" | |
| # head counts lines, not merges: multi-line %b bodies eat into the budget, | |
| # so this can yield fewer than MAX_PER_REPO entries. Under-full is fine. | |
| merged="$(git -C "$repo" log --merges --first-parent "$branch" --since="$cutoff" \ | |
| --format='%as%x09%s%x09%b' 2>/dev/null | head -"$MAX_PER_REPO")" || true | |
| if [[ -n "$merged" ]]; then | |
| local subject body | |
| while IFS=$'\t' read -r date subject body; do | |
| # %b is multi-line; continuation lines lack the leading ISO date — skip. | |
| [[ "$date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || continue | |
| title="$(merge_title "$subject" "$body")" | |
| printf '%s|%s|%s\n' "$name" "$date" "$title" | |
| done <<<"$merged" | |
| return 0 | |
| fi | |
| # 4. Latest commit | |
| entry="$(git -C "$repo" log -1 --format='%as|%s' 2>/dev/null)" || return 0 | |
| date="${entry%%|*}" | |
| [[ -n "$date" && ! "$date" < "$cutoff" ]] && printf '%s|%s\n' "$name" "$entry" | |
| return 0 | |
| } | |
| # --- GitHub remote ----------------------------------------------------------- | |
| entry_remote() { | |
| local slug="$1" file="$2" cutoff="$3" ref="${4:-}" name branch tmp candidate entry date title | |
| name="${slug##*/}" | |
| branch="$(gh_api "repos/$slug" '.default_branch')" || true | |
| [[ -z "$branch" ]] && return 0 | |
| # 1. Changelog ($ref overrides the default branch for explicit file URLs) | |
| tmp="$(mktemp)" | |
| local found="" | |
| if [[ -n "$file" ]]; then | |
| curl -fsSL --max-time 10 "https://raw.githubusercontent.com/$slug/${ref:-$branch}/$file" -o "$tmp" 2>/dev/null && found="$file" | |
| else | |
| for candidate in CHANGELOG.md CHANGELOG Changelog.md changelog.md CHANGES.md NEWS.md; do | |
| if curl -fsSL --max-time 10 "https://raw.githubusercontent.com/$slug/$branch/$candidate" -o "$tmp" 2>/dev/null; then | |
| found="$candidate"; break | |
| fi | |
| done | |
| fi | |
| local result="" | |
| if [[ -n "$found" ]]; then | |
| entry="$(parse_changelog "$tmp")" | |
| if [[ -n "$entry" ]]; then | |
| date="${entry%%|*}"; title="${entry#*|}" | |
| [[ -z "$date" ]] && date="$(gh_api "repos/$slug/commits?path=$found&per_page=1${ref:+&sha=$ref}" '.[0].commit.committer.date' | cut -dT -f1)" | |
| [[ -n "$date" && ! "$date" < "$cutoff" ]] && result="$name|$date|$title" | |
| fi | |
| fi | |
| rm -f "$tmp" | |
| if [[ -n "$result" ]]; then | |
| printf '%s\n' "$result" | |
| return 0 | |
| fi | |
| # 2. Releases | |
| entry="$(gh_api "repos/$slug/releases?per_page=1" \ | |
| '.[0] | select(.published_at != null) | "\(.published_at)|\(.name // .tag_name)"')" || true | |
| if [[ -n "$entry" ]]; then | |
| date="$(cut -dT -f1 <<<"${entry%%|*}")"; title="released ${entry#*|}" | |
| if [[ -n "$date" && ! "$date" < "$cutoff" ]]; then | |
| printf '%s|%s|%s\n' "$name" "$date" "$title" | |
| return 0 | |
| fi | |
| fi | |
| # 3. PRs merged to the default branch within the window | |
| local merged | |
| merged="$(gh_api "search/issues?q=repo:$slug+is:pr+is:merged+merged:%3E%3D$cutoff&per_page=$MAX_PER_REPO" \ | |
| '.items[] | "\(.closed_at)|\(.title)"')" || true | |
| if [[ -n "$merged" ]]; then | |
| while IFS='|' read -r date title; do | |
| [[ -z "$date" ]] && continue | |
| printf '%s|%s|%s\n' "$name" "$(cut -dT -f1 <<<"$date")" "$title" | |
| done <<<"$merged" | |
| return 0 | |
| fi | |
| # 4. Latest commit | |
| entry="$(gh_api "repos/$slug/commits?per_page=1" '"\(.[0].commit.committer.date)|\(.[0].commit.message)"' | head -1)" || true | |
| [[ -z "$entry" || "$entry" == "null|null" ]] && return 0 | |
| date="$(cut -dT -f1 <<<"${entry%%|*}")" | |
| [[ -n "$date" && ! "$date" < "$cutoff" ]] && printf '%s|%s|%s\n' "$name" "$date" "${entry#*|}" | |
| return 0 | |
| } | |
| # Route one config source line to entry_local or entry_remote. | |
| entries_for_source() { | |
| local src="$1" cutoff="$2" slug rest | |
| case "$src" in | |
| /*|"~"*) | |
| entry_local "${src/#\~/$HOME}" "$cutoff" ;; | |
| *github.com*|*githubusercontent.com*) | |
| rest="${src#*github.com/}" | |
| rest="${rest#*githubusercontent.com/}" | |
| rest="${rest%.git}" | |
| slug="$(cut -d/ -f1,2 <<<"$rest")" | |
| # blob/raw URLs carry branch + file path after the slug | |
| local path ref="" file="" | |
| path="${rest#"$slug"}"; path="${path#/}" | |
| if [[ -n "$path" ]]; then | |
| path="${path#blob/}" | |
| ref="${path%%/*}" # honor the URL's branch segment | |
| file="${path#*/}" | |
| fi | |
| entry_remote "$slug" "$file" "$cutoff" "$ref" ;; | |
| */*) | |
| entry_remote "$src" "" "$cutoff" ;; | |
| *) | |
| echo "skipping unrecognized source: $src" >&2 ;; | |
| esac | |
| } | |
| main() { | |
| command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; } | |
| [[ -f "$SETTINGS" ]] || { echo "settings file not found: $SETTINGS" >&2; exit 1; } | |
| # Once, not per-call: gh_api runs in command substitutions, so a flag set | |
| # there wouldn't persist. Anonymous GitHub API is limited to 60 req/hour; | |
| # many remote sources can silently come back empty when it's exhausted. | |
| command -v gh >/dev/null || \ | |
| echo "gh not found; remote sources use the anonymous GitHub API (60 req/hour)" >&2 | |
| local cutoff verbs_json="[]" pool_verbs=() sources=() mode_cfg="" | |
| cutoff="$(cutoff_date)" | |
| # Parse config: `verb:` lines build the custom pool, `mode:` sets the | |
| # spinnerVerbs mode (append|replace); the rest are sources. No config file | |
| # means no sources — nothing to scan implicitly. | |
| if [[ -f "$CONFIG" ]]; then | |
| local line | |
| while IFS= read -r line; do | |
| line="${line%%#*}" | |
| line="$(sed -E 's/^[[:space:]]+|[[:space:]]+$//g' <<<"$line")" | |
| [[ -z "$line" ]] && continue | |
| if [[ "$line" == verb:* ]]; then | |
| pool_verbs+=("$(sed -E 's/^verb:[[:space:]]*//' <<<"$line")") | |
| elif [[ "$line" == mode:* ]]; then | |
| mode_cfg="$(sed -E 's/^mode:[[:space:]]*//' <<<"$line")" | |
| else | |
| sources+=("$line") | |
| fi | |
| done < "$CONFIG" | |
| fi | |
| # Custom pool verbs: appended standalone AND used as the rotation pool. | |
| if [[ ${#pool_verbs[@]} -gt 0 ]]; then | |
| VERBS=("${pool_verbs[@]}") | |
| local v | |
| for v in "${pool_verbs[@]}"; do | |
| verbs_json="$(jq --arg v "$v" '. + [$v]' <<<"$verbs_json")" | |
| done | |
| fi | |
| # Each source's lookup chain is independent network/git I/O (changelog | |
| # probe, releases, PR search, commits), so fan them out concurrently | |
| # instead of paying their latency serially. Order across sources doesn't | |
| # matter for the final pool, only within a source's own entries. Capped at | |
| # MAX_PARALLEL in-flight sources at a time so a long source list doesn't | |
| # fire a burst of simultaneous GitHub API calls (anonymous access is | |
| # limited to 60 req/hour, and GitHub also applies secondary concurrent- | |
| # request limits even when authenticated). | |
| local src entry name date title verb count_for_repo i src_files=() | |
| tmpdir="$(mktemp -d)" | |
| i=0 | |
| for src in ${sources[@]+"${sources[@]}"}; do | |
| local sf="$tmpdir/$i" | |
| src_files+=("$sf") | |
| ( entries_for_source "$src" "$cutoff" > "$sf" 2>>"$tmpdir/stderr" ) & | |
| i=$((i + 1)) | |
| if (( i % MAX_PARALLEL == 0 )); then | |
| wait | |
| fi | |
| done | |
| wait | |
| [[ -f "$tmpdir/stderr" ]] && cat "$tmpdir/stderr" >&2 | |
| for sf in ${src_files[@]+"${src_files[@]}"}; do | |
| count_for_repo=0 | |
| while IFS= read -r entry; do | |
| [[ -z "$entry" ]] && continue | |
| IFS='|' read -r name date title <<<"$entry" | |
| # ISO dates compare lexicographically; skip stale or undated entries. | |
| [[ -z "$date" || "$date" < "$cutoff" ]] && continue | |
| # Strip control + Unicode spoofing chars (see sanitize_title). | |
| title="$(sanitize_title <<<"$title")" | |
| if [[ $count_for_repo -ge $MAX_PER_REPO ]]; then | |
| echo "capping $name at $MAX_PER_REPO entries" >&2 | |
| break | |
| fi | |
| [[ ${#title} -gt $MAX_TITLE_LEN ]] && title="${title:0:$MAX_TITLE_LEN}…" | |
| verb="$(pick_verb "$name") [$name][$date]: \"$title\"" | |
| verbs_json="$(jq --arg v "$verb" '. + [$v]' <<<"$verbs_json")" | |
| count_for_repo=$((count_for_repo + 1)) | |
| done < "$sf" | |
| done | |
| local count | |
| count="$(jq 'length' <<<"$verbs_json")" | |
| if [[ "$count" -eq 0 ]]; then | |
| echo "no custom verbs and no activity in the last $MAX_AGE_DAYS days; leaving $SETTINGS unchanged" >&2 | |
| touch "$MARKER" | |
| exit 0 | |
| fi | |
| # "append" mixes into Claude Code's large built-in pool (custom verbs show | |
| # up rarely); "replace" shows only these. Env overrides config. | |
| local mode | |
| mode="${SPINNER_MODE:-${mode_cfg:-append}}" | |
| if [[ "$mode" != "append" && "$mode" != "replace" ]]; then | |
| echo "invalid mode '$mode' (expected append or replace); using append" >&2 | |
| mode="append" | |
| fi | |
| # Same-directory temp file so the mv is a true atomic rename (no cross-device | |
| # copy out of tmpfs), and mktemp's 0600 mode doesn't stick to settings.json. | |
| # cleanup_on_exit (trapped once, near the top of the script) removes it on | |
| # any exit path, including a TERM from an external watchdog/timeout. | |
| local perms | |
| tmp="$(mktemp "${SETTINGS}.XXXXXX")" | |
| case "$(uname -s)" in | |
| Darwin) perms="$(stat -f %Lp "$SETTINGS")" ;; | |
| *) perms="$(stat -c %a "$SETTINGS")" ;; | |
| esac | |
| chmod "$perms" "$tmp" | |
| jq --argjson verbs "$verbs_json" --arg mode "$mode" \ | |
| '.spinnerVerbs = {mode: $mode, verbs: $verbs}' \ | |
| "$SETTINGS" > "$tmp" || exit 1 # trap cleans up the temp file | |
| mv "$tmp" "$SETTINGS" | |
| # Only mark the run fresh once settings.json has actually been rewritten, | |
| # so a failure earlier in main() (missing jq/gh, bad config, mv failure) | |
| # leaves the marker stale and the next SessionStart hook retries instead | |
| # of silently skipping for the rest of the TTL window. | |
| touch "$MARKER" | |
| echo "wrote $count spinner verbs to $SETTINGS (mode: $mode)" | |
| } | |
| # SessionStart hooks have a short timeout budget, but regeneration involves | |
| # per-source network I/O that can legitimately run long (slow GitHub API, | |
| # many sources). Rather than risk the hook getting killed mid-write, the | |
| # hook invocation only ever does a cheap marker check: if a run completed | |
| # within TTL_HOURS, skip; otherwise it acquires LOCKDIR (an atomic `mkdir`, | |
| # so two hook invocations racing at startup can't both fork a worker) and | |
| # forks the real work into the background, detached from the hook's process | |
| # group, returning immediately. The forked worker re-execs with | |
| # SPINNER_BG_WORKER=1 so it runs main() for real under a watchdog timeout | |
| # (backgrounding removed the SessionStart hook's own timeout, which used to | |
| # bound a hung `gh`/network call) and only touches MARKER — inside main(), | |
| # see above — once settings.json has actually been rewritten. | |
| if [[ "${SPINNER_BG_WORKER:-}" == "1" ]]; then | |
| main "$@" & | |
| worker_pid=$! | |
| ( sleep "$BG_TIMEOUT_SECS"; kill -TERM "$worker_pid" 2>/dev/null ) & | |
| watchdog_pid=$! | |
| wait "$worker_pid" | |
| status=$? | |
| kill -TERM "$watchdog_pid" 2>/dev/null | |
| exit "$status" | |
| elif marker_fresh; then | |
| echo "spinner verbs refreshed within the last ${TTL_HOURS}h; skipping" >&2 | |
| elif mkdir -p "$(dirname "$MARKER")" && mkdir "$LOCKDIR" 2>/dev/null; then | |
| nohup env SPINNER_BG_WORKER=1 "$0" "$@" >>"$LOG" 2>&1 & | |
| disown | |
| echo "spinner verb regeneration started in background (log: $LOG)" | |
| else | |
| echo "spinner verb regeneration already in progress; skipping" >&2 | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment