Created
June 13, 2026 17:07
-
-
Save ybj14/819ea5b0267a4369a0285405899df824 to your computer and use it in GitHub Desktop.
fluent-sync.sh — sync Fluent learning data + plugin fork across machines
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 | |
| # | |
| # fluent-sync.sh — keep your Fluent learning data in sync across machines | |
| # using a PRIVATE GitHub repository. | |
| # | |
| # init Machine 1 (first time): create the private repo, commit, push. | |
| # clone Machine 2 (first time): clone the repo into the data dir. | |
| # push After a session: commit local changes and upload. | |
| # pull Before/after switching machines: download latest. | |
| # sync The everyday command: pull --rebase then push. | |
| # status Show working-tree + divergence vs remote. | |
| # log Recent commits. | |
| # | |
| # Env: FLUENT_DATA_DIR (default ~/.claude/fluent-data) | |
| # REPO_NAME (default fluent-data) | |
| # | |
| set -euo pipefail | |
| DATA_DIR="${FLUENT_DATA_DIR:-$HOME/.claude/fluent-data}" | |
| REPO_NAME="${REPO_NAME:-fluent-data}" | |
| BRANCH="main" | |
| die() { echo "fluent-sync: error: $*" >&2; exit 1; } | |
| need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"; } | |
| ensure_tools() { | |
| need_cmd git | |
| need_cmd gh | |
| gh auth status >/dev/null 2>&1 || die "gh is not logged in. Run: gh auth login" | |
| # Let git use gh's token for github.com auth (idempotent; needed on a fresh machine). | |
| gh auth setup-git >/dev/null 2>&1 || true | |
| } | |
| github_user() { gh api user --jq .login 2>/dev/null | tr -d '[:space:]'; } | |
| remote_url() { echo "https://github.com/$(github_user)/${REPO_NAME}.git"; } | |
| # Set a LOCAL identity if none is configured, so commits always succeed. | |
| ensure_identity() { | |
| local u; u="$(github_user)" | |
| git config user.name >/dev/null 2>&1 || git config user.name "$u" | |
| git config user.email >/dev/null 2>&1 || git config user.email "${u}@users.noreply.github.com" | |
| } | |
| write_gitignore() { | |
| mkdir -p "$DATA_DIR" | |
| cat > "$DATA_DIR/.gitignore" <<'EOF' | |
| # Internal backups produced by Fluent hooks — the tracked JSON DBs are the source of truth. | |
| *.backup-* | |
| .backups/ | |
| # Generated session reports (regenerable from the DBs). | |
| results/ | |
| # OS noise. | |
| .DS_Store | |
| EOF | |
| } | |
| # ---- Machine 1: create + first push ---------------------------------------- | |
| cmd_init() { | |
| ensure_tools | |
| [[ -d "$DATA_DIR/.git" ]] && die "$DATA_DIR is already a git repo (use 'push' or 'pull')." | |
| echo ">> Initialising Fluent sync in: $DATA_DIR" | |
| cd "$DATA_DIR" | |
| write_gitignore | |
| git init -q -b "$BRANCH" | |
| ensure_identity | |
| git add -A | |
| git commit -q -m "fluent-sync: initial import" || echo "(nothing to commit)" | |
| local url user; user="$(github_user)"; url="$(remote_url)" | |
| echo ">> Creating private GitHub repo: $user/$REPO_NAME" | |
| gh repo create "$REPO_NAME" --private \ | |
| --description "Fluent language-learning data (auto-synced)" \ | |
| || die "could not create repo '$REPO_NAME' (already exists?)" | |
| git remote add origin "$url" | |
| git push -u origin "$BRANCH" | |
| echo | |
| echo "Done. Your Fluent data is now backed up (PRIVATE) at:" | |
| echo " $url" | |
| echo | |
| echo "On your other machine, copy this script there and run: fluent-sync.sh clone" | |
| } | |
| # ---- Machine 2: clone ------------------------------------------------------- | |
| cmd_clone() { | |
| ensure_tools | |
| local url; url="$(remote_url)" | |
| echo ">> Cloning $url into $DATA_DIR" | |
| if [[ -e "$DATA_DIR" ]]; then | |
| local bak="${DATA_DIR%/}.presync.$(date +%Y%m%d-%H%M%S)" | |
| mv "$DATA_DIR" "$bak" | |
| echo ">> Existing data dir moved aside to: $bak" | |
| echo " (review it, then delete once the clone looks correct)" | |
| fi | |
| mkdir -p "$(dirname "$DATA_DIR")" | |
| git clone --branch "$BRANCH" "$url" "$DATA_DIR" | |
| echo | |
| echo "Done. Latest Fluent data is now in $DATA_DIR" | |
| } | |
| # ---- commit + upload -------------------------------------------------------- | |
| cmd_push() { | |
| ensure_tools | |
| cd "$DATA_DIR" | |
| ensure_identity | |
| git add -A | |
| if git diff --cached --quiet; then | |
| echo "No local changes to push." | |
| else | |
| git commit -q -m "fluent-sync: update $(date -u +%FT%TZ)" | |
| fi | |
| echo ">> Integrating any remote changes (rebase) before push..." | |
| git pull --rebase --autostash origin "$BRANCH" | |
| git push origin "$BRANCH" | |
| echo "Pushed." | |
| } | |
| # ---- download latest -------------------------------------------------------- | |
| cmd_pull() { | |
| ensure_tools | |
| cd "$DATA_DIR" | |
| echo ">> Pulling latest (rebase + autostash)..." | |
| git pull --rebase --autostash origin "$BRANCH" | |
| echo "Updated." | |
| } | |
| cmd_sync() { cmd_pull; cmd_push; } | |
| cmd_status(){ ensure_tools; cd "$DATA_DIR"; ensure_identity; git status -sb; } | |
| cmd_log() { cd "$DATA_DIR"; git log --oneline -10; } | |
| # --------------------------------------------------------------------------- | |
| # plugin subcommand — sync Fluent PLUGIN code (skills/hooks) via your fork. | |
| # Edits made through Claude land in the INSTALLED cache; this publishes them. | |
| # --------------------------------------------------------------------------- | |
| FORK_REPO="${FORK_REPO:-fluent}" | |
| DEV_CLONE="${FLUENT_DEV_CLONE:-$HOME/projects/fluent}" | |
| fork_url() { echo "https://github.com/$(github_user)/${FORK_REPO}.git"; } | |
| cache_hooks_dir(){ find "$HOME/.claude/plugins/cache" -path '*/fluent/*/hooks/fluent_paths.py' -print -quit 2>/dev/null | head -1; } | |
| cache_root() { local h; h="$(cache_hooks_dir)"; [[ -n "$h" ]] && dirname "$h"; } | |
| ensure_dev_clone() { | |
| if [[ ! -d "$DEV_CLONE/.git" ]]; then | |
| echo ">> Cloning fork into $DEV_CLONE" | |
| mkdir -p "$(dirname "$DEV_CLONE")" | |
| git clone "$(fork_url)" "$DEV_CLONE" | |
| fi | |
| } | |
| # Count installed-cache files that differ from the dev clone (the real-edit set: | |
| # skill MDs + hooks.json + fluent_paths.py; NOT plugin.json — a layout artifact). | |
| cache_divergence() { | |
| local root clone ndiff=0 | |
| root="$(cache_root)" || return 0 | |
| [[ -n "$root" ]] || return 0 | |
| for f in "$root"/skills/*/SKILL.md; do | |
| [[ -f "$f" ]] || continue | |
| local name; name="$(basename "$(dirname "$f")")" | |
| clone="$DEV_CLONE/.claude/skills/$name/SKILL.md" | |
| { [[ ! -f "$clone" ]] || ! diff -q "$f" "$clone" >/dev/null 2>&1; } && ndiff=$((ndiff+1)) | |
| done | |
| for hf in hooks.json fluent_paths.py; do | |
| [[ -f "$root/hooks/$hf" ]] || continue | |
| local c="$DEV_CLONE/.claude/hooks/$hf" | |
| { [[ ! -f "$c" ]] || ! diff -q "$root/hooks/$hf" "$c" >/dev/null 2>&1; } && ndiff=$((ndiff+1)) | |
| done | |
| echo "$ndiff" | |
| } | |
| cmd_plugin_capture() { | |
| ensure_tools | |
| local root; root="$(cache_root)" | |
| [[ -n "$root" ]] || die "installed Fluent cache not found under ~/.claude/plugins/cache" | |
| ensure_dev_clone | |
| echo ">> Capturing installed-cache edits from: $root" | |
| for f in "$root"/skills/*/SKILL.md; do | |
| [[ -f "$f" ]] || continue | |
| local name; name="$(basename "$(dirname "$f")")" | |
| cp "$f" "$DEV_CLONE/.claude/skills/$name/SKILL.md" | |
| done | |
| for hf in hooks.json fluent_paths.py; do | |
| [[ -f "$root/hooks/$hf" ]] && cp "$root/hooks/$hf" "$DEV_CLONE/.claude/hooks/$hf" | |
| done | |
| cd "$DEV_CLONE"; ensure_identity | |
| git add .claude/skills .claude/hooks/hooks.json .claude/hooks/fluent_paths.py | |
| if git diff --cached --quiet; then | |
| echo "No changes to capture (installed cache already matches the fork)."; return | |
| fi | |
| git commit -q -m "fluent-sync: capture installed skill/hook edits ($(date -u +%FT%TZ))" | |
| git push origin main | |
| echo "Pushed to $(fork_url)" | |
| echo ">> On OTHER machines: 'fluent-sync.sh plugin pull', then /plugin to update fluent." | |
| } | |
| cmd_plugin_pull() { | |
| ensure_tools; ensure_dev_clone | |
| cd "$DEV_CLONE" | |
| git pull --rebase --autostash origin main | |
| echo "Dev clone updated: $DEV_CLONE" | |
| echo ">> To apply to the INSTALLED plugin here: use /plugin to update/reinstall fluent." | |
| } | |
| cmd_plugin_status() { | |
| ensure_tools | |
| local u; u="$(github_user)" | |
| echo "== Fork $u/$FORK_REPO ==" | |
| gh api "repos/$u/$FORK_REPO/commits" --jq '.[0]|" HEAD \(.sha[0:7]) \(.commit.message|split("\n")[0])"' 2>/dev/null || echo " (could not read fork)" | |
| echo | |
| echo "== Dev clone $DEV_CLONE ==" | |
| if [[ -d "$DEV_CLONE/.git" ]]; then | |
| git -C "$DEV_CLONE" log --oneline -1 | sed 's/^/ /' | |
| git -C "$DEV_CLONE" status -sb | head -1 | sed 's/^/ /' | |
| else | |
| echo " (absent — run 'fluent-sync.sh plugin pull' to create it)" | |
| fi | |
| echo | |
| echo "== Installed cache vs fork ==" | |
| local n; n="$(cache_divergence)" | |
| echo " ${n:-0} file(s) differ between installed cache and fork" | |
| [[ "${n:-0}" -gt 0 ]] && echo " -> run 'fluent-sync.sh plugin capture' to publish them" | |
| } | |
| cmd_plugin() { | |
| case "${1:-}" in | |
| capture) cmd_plugin_capture ;; | |
| pull) cmd_plugin_pull ;; | |
| status) cmd_plugin_status ;; | |
| *) cat <<EOF | |
| Usage: fluent-sync.sh plugin <action> | |
| status show fork / dev-clone / installed-cache divergence | |
| capture copy installed skill+hook edits into the fork, commit, push | |
| (run this after editing skills/hooks through Claude) | |
| pull pull fork latest into the dev clone | |
| (default \$FLUENT_DEV_CLONE = ~/projects/fluent) | |
| Env: FLUENT_DEV_CLONE (default ~/projects/fluent), FORK_REPO (default fluent) | |
| Note: applying fork changes to the INSTALLED plugin is done via Claude Code's | |
| /plugin command (update or reinstall fluent), not by this script. | |
| EOF | |
| exit 1 ;; | |
| esac | |
| } | |
| case "${1:-}" in | |
| init) cmd_init ;; | |
| clone) cmd_clone ;; | |
| push) cmd_push ;; | |
| pull) cmd_pull ;; | |
| sync) cmd_sync ;; | |
| status) cmd_status ;; | |
| log) cmd_log ;; | |
| plugin) shift; cmd_plugin "$@" ;; | |
| *) cat <<EOF | |
| Usage: fluent-sync.sh <command> | |
| DATA (learning progress) — synced via a private repo: | |
| init first machine: create private repo + push | |
| clone other machine: clone repo into the data dir | |
| push commit + upload local changes | |
| pull download latest from GitHub | |
| sync pull then push (everyday command) | |
| status show divergence vs remote | |
| log recent commits | |
| PLUGIN (skills/hooks) — synced via your Fluent fork (run 'fluent-sync.sh plugin' for detail): | |
| plugin status fork / dev-clone / installed-cache divergence | |
| plugin capture publish installed skill+hook edits to the fork | |
| plugin pull pull fork latest into the dev clone | |
| Env: FLUENT_DATA_DIR (default ~/.claude/fluent-data), REPO_NAME (default fluent-data), | |
| FLUENT_DEV_CLONE (default ~/projects/fluent), FORK_REPO (default fluent) | |
| EOF | |
| exit 1 ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment