Skip to content

Instantly share code, notes, and snippets.

@ybj14
Created June 13, 2026 17:07
Show Gist options
  • Select an option

  • Save ybj14/819ea5b0267a4369a0285405899df824 to your computer and use it in GitHub Desktop.

Select an option

Save ybj14/819ea5b0267a4369a0285405899df824 to your computer and use it in GitHub Desktop.
fluent-sync.sh — sync Fluent learning data + plugin fork across machines
#!/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