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
60 changes: 59 additions & 1 deletion bin/gs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ gs - gentle-shell on main, plus one disposable worktree per PR
(workshop: local clones; elsewhere: GitHub)
GS_SYNC_AGENTS=a,b scopes gentle-ai sync to those runtimes;
GS_SKIP_AI_SYNC=1 skips it (binaries only)
Then pulls the dotfiles clone (clean, on main only) and
installs its fish 10-core.fish: symlink, or home-manager
switch when Home Manager owns it (GS_HM_CONFIG picks the
flake config); GS_SKIP_DOTFILES=1 skips this step
gs ls List open worktrees
gs done Remove the current worktree and its merged local branch
(drops odd/tasks records; refuses if real work would be lost)
Expand Down Expand Up @@ -265,8 +269,60 @@ changed_part() {
if [ -n "$2" ] && [ -n "$3" ] && [ "$2" != "$3" ]; then printf ' %s (%s -> %s)' "$1" "$2" "$3"; fi
}

# sync_dotfiles — pull the dotfiles clone and install its fish 10-core.fish, so
# hosts never keep hand-edited copies. Sets dot_change (" dotfiles (a -> b)").
# Never fails the sync: every problem is a status line.
sync_dotfiles() {
local dir="${CORTEX_DOTFILES_DIR:-$HOME/dev/barbatdev/cortex/cortex-dotfiles}"
local target="$HOME/.config/fish/conf.d/10-core.fish" src branch old new out cfg
if [ "${GS_SKIP_DOTFILES:-}" = 1 ]; then
printf ' %-34s skipped (GS_SKIP_DOTFILES=1)\n' dotfiles; return 0
fi
if [ ! -d "$dir/.git" ]; then
printf ' %-34s skipped (no clone at %s)\n' dotfiles "$dir"; return 0
fi
# Never touch work in progress: only a clean clone on main follows origin.
if [ -n "$(git -C "$dir" status --porcelain 2>/dev/null)" ]; then
printf ' %-34s skipped (%s has local changes)\n' dotfiles "$dir"; return 0
fi
branch=$(git -C "$dir" branch --show-current 2>/dev/null || true)
if [ "$branch" != main ]; then
printf ' %-34s skipped (%s is on %s, not main)\n' dotfiles "$dir" "${branch:-detached HEAD}"; return 0
fi
old=$(git -C "$dir" rev-parse --short HEAD)
if ! out=$(git -C "$dir" pull -q --ff-only origin main 2>&1); then
printf ' %-34s FAILED\n' dotfiles; printf '%s\n' "$out" | sed 's/^/ /'; return 0
fi
new=$(git -C "$dir" rev-parse --short HEAD)
printf ' %-34s ok\n' dotfiles
[ "$old" != "$new" ] && dot_change=" dotfiles ($old -> $new)"

src="$dir/fish/conf.d/10-core.fish"
[ -f "$src" ] || return 0
# Home Manager owns the file when it links into the nix store: switch, never relink.
if [ -L "$target" ] && case "$(readlink "$target")" in "${GS_HM_STORE_PREFIX:-/nix/store}"/*) true ;; *) false ;; esac; then
if ! command -v home-manager >/dev/null 2>&1; then
printf ' %-34s skipped (Home Manager link but no home-manager on PATH)\n' '10-core.fish'; return 0
fi
if [ "$(uname)" = Darwin ]; then cfg=jbarbat; else cfg=jbarbat-linux; fi
run_quiet 'home-manager switch' home-manager switch --flake "$dir#${GS_HM_CONFIG:-$cfg}" -b pre-gs
return 0
fi
if [ -L "$target" ] && [ "$(readlink "$target")" = "$src" ]; then
printf ' %-34s ok\n' '10-core.fish'; return 0
fi
mkdir -p "$(dirname "$target")"
if [ -e "$target" ] || [ -L "$target" ]; then
local backup="$target.pre-gs"
{ [ -e "$backup" ] || [ -L "$backup" ]; } && backup="$target.pre-gs.$(date +%s)"
mv "$target" "$backup"
fi
ln -s "$src" "$target"
printf ' %-34s linked\n' '10-core.fish'
}

cmd_sync() {
local ai_files="" repo shell bin="$HOME/.local/share/gentle-ai/main" reg="$HOME/.pi/gentle-ai/dev-binary.json" name
local ai_files="" dot_change="" repo shell bin="$HOME/.local/share/gentle-ai/main" reg="$HOME/.pi/gentle-ai/dev-binary.json" name
# Versions before the sync power the closing message: restart only on change.
local shell_before ai_before engram_before
shell_before=$(shell_version); ai_before=$(ai_version); engram_before=$(engram_version)
Expand Down Expand Up @@ -364,6 +420,7 @@ cmd_sync() {
ai_files=$(sed -n 's/^Sync actions executed: \([0-9][0-9]*\) files* changed.*/\1/p' "$ai_out" | head -1)
rm -f "$ai_out"
fi
sync_dotfiles
# gs sync is the only upgrade path; older homemade scripts drift the hosts apart.
for name in "$HOME/.local/bin/gentle-maintainer-upgrade" "$HOME/.local/bin/gentle-sync.sh" \
/usr/bin/gentle-sync.sh /usr/local/bin/gentle-upgrade.sh /bin/gentle-upgrade.sh; do
Expand Down Expand Up @@ -397,6 +454,7 @@ cmd_sync() {
if [ -n "$ai_files" ] && [ "$ai_files" -gt 0 ]; then
changed="$changed managed configs ($ai_files files)"
fi
changed="$changed$dot_change"
if [ -n "$changed" ]; then
printf 'changed:%s\n Restart Pi to pick them up.\n' "$changed"
else
Expand Down
4 changes: 4 additions & 0 deletions fish/conf.d/99-local.fish.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@
# set -gx GS_SKIP_AI_SYNC 1
# 10-core.fish defaults GENTLE_AI_CHANNEL to beta; override only on a host that must differ:
# set -gx GENTLE_AI_CHANNEL stable
# gs sync also pulls the dotfiles clone (clean, on main) and installs 10-core.fish.
# set -gx CORTEX_DOTFILES_DIR "$HOME/dev/barbatdev/cortex/cortex-dotfiles"
# set -gx GS_HM_CONFIG jbarbat-linux # flake config when Home Manager owns 10-core.fish
# set -gx GS_SKIP_DOTFILES 1
3 changes: 3 additions & 0 deletions local/env.zsh.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@
# export GS_SYNC_AGENTS="claude-code,opencode,pi"
# Binaries only, no managed-config sync:
# export GS_SKIP_AI_SYNC=1
# gs sync also pulls the dotfiles clone (clean, on main) and installs 10-core.fish.
# export GS_HM_CONFIG="jbarbat" # flake config when Home Manager owns 10-core.fish
# export GS_SKIP_DOTFILES=1
61 changes: 60 additions & 1 deletion scripts/test-gs-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ esac
exit 0
EOF
chmod +x "$shims/_shim"
for name in gentle-shell pi go; do ln -s _shim "$shims/$name"; done
for name in gentle-shell pi go home-manager; do ln -s _shim "$shims/$name"; done
ai_dir="$ws/ai"; mkdir -p "$ai_dir"; ln -s "$shims/_shim" "$ai_dir/gentle-ai"
printf '{"schema":"gentle-pi.dev-binary/v1","path":"%s"}\n' "$ai_dir/gentle-ai" >"$home/.pi/gentle-ai/dev-binary.json"

Expand Down Expand Up @@ -77,4 +77,63 @@ out=$(run_sync) || fail 'gs sync exited non-zero with legacy script' "$out"
[[ "$out" == *"WARN: legacy upgrade script $home/.local/bin/gentle-maintainer-upgrade"* ]] || fail 'legacy script not flagged' "$out"
[[ -f "$home/.local/bin/gentle-maintainer-upgrade" ]] || fail 'legacy script was removed' "$out"

# 6. Dotfiles step. A real local clone with a bare origin, so pull works offline.
dot="$home/dev/barbatdev/cortex/cortex-dotfiles"
origin="$ws/origin.git"
target="$home/.config/fish/conf.d/10-core.fish"
g() { git -c user.name=t -c user.email=t@t -c init.defaultBranch=main "$@"; }
g init -q --bare "$origin"
g clone -q "$origin" "$dot" 2>/dev/null
mkdir -p "$dot/fish/conf.d" && echo 'core v1' >"$dot/fish/conf.d/10-core.fish"
g -C "$dot" add -A && g -C "$dot" commit -qm v1 && g -C "$dot" push -q origin HEAD:main
dot_status() { grep -E '^ (dotfiles|10-core.fish) ' <<<"$1" || true; }

# 6a. GS_SKIP_DOTFILES=1 skips everything.
out=$(run_sync GS_SKIP_DOTFILES=1) || fail 'gs sync failed with GS_SKIP_DOTFILES' "$out"
[[ "$(dot_status "$out")" == *'skipped (GS_SKIP_DOTFILES=1)'* ]] || fail 'dotfiles skip line missing' "$out"
[[ ! -e "$target" && ! -L "$target" ]] || fail '10-core.fish installed despite skip' "$out"

# 6b. No clone: skip and keep going.
out=$(run_sync CORTEX_DOTFILES_DIR="$ws/nowhere") || fail 'gs sync failed without clone' "$out"
[[ "$(dot_status "$out")" == *"skipped (no clone at $ws/nowhere)"* ]] || fail 'no-clone skip missing' "$out"
[[ "$out" == *'versions:'* ]] || fail 'sync stopped after missing clone' "$out"

# 6c. Upstream moves; the clean clone on main is pulled and the hint names it.
up="$ws/upstream"; g clone -q "$origin" "$up"
echo 'core v2' >"$up/fish/conf.d/10-core.fish"; g -C "$up" commit -qam v2 && g -C "$up" push -q origin HEAD:main
old=$(git -C "$dot" rev-parse --short HEAD); new=$(git -C "$up" rev-parse --short HEAD)
mkdir -p "$(dirname "$target")"; echo 'host copy' >"$target"
out=$(run_sync) || fail 'gs sync failed on dotfiles pull' "$out"
[[ "$(git -C "$dot" rev-parse --short HEAD)" == "$new" ]] || fail 'clone was not pulled' "$out"
[[ "$out" == *"dotfiles ($old -> $new)"* ]] || fail 'restart hint misses dotfiles change' "$out"
# 6d. A regular host copy is backed up byte-for-byte and replaced by a link to the clone.
[[ "$(<"$target.pre-gs")" == 'host copy' ]] || fail 'host copy not backed up' "$(ls -la "$(dirname "$target")")"
[[ -L "$target" && "$(readlink "$target")" == "$dot/fish/conf.d/10-core.fish" ]] || fail '10-core.fish not linked to the clone' "$(ls -la "$target")"
[[ "$(dot_status "$out")" == *'10-core.fish'*'linked'* ]] || fail 'link status line missing' "$out"
# 6e. Second run: nothing to pull, link already right.
out=$(run_sync) || fail 'second gs sync failed' "$out"
[[ "$(dot_status "$out")" == *'10-core.fish'*'ok'* ]] || fail 'second run should be a no-op' "$out"
[[ "$out" != *'dotfiles ('* ]] || fail 'no-op pull reported as change' "$out"

# 6f. Local changes or another branch: clone untouched.
echo dirty >>"$dot/fish/conf.d/10-core.fish"
head=$(git -C "$dot" rev-parse HEAD)
out=$(run_sync) || fail 'gs sync failed with dirty clone' "$out"
[[ "$(dot_status "$out")" == *"skipped ($dot has local changes)"* ]] || fail 'dirty skip missing' "$out"
[[ "$(tail -1 "$dot/fish/conf.d/10-core.fish")" == dirty && "$(git -C "$dot" rev-parse HEAD)" == "$head" ]] || fail 'dirty clone was touched' "$out"
git -C "$dot" checkout -q -- . && git -C "$dot" checkout -q -b topic
out=$(run_sync) || fail 'gs sync failed on topic branch' "$out"
[[ "$(dot_status "$out")" == *"skipped ($dot is on topic, not main)"* ]] || fail 'branch skip missing' "$out"
git -C "$dot" checkout -q main

# 6g. Home Manager owns 10-core.fish: switch through the flake, never relink.
store="$ws/nix/store"; mkdir -p "$store/hm/fish"; echo hm >"$store/hm/fish/10-core.fish"
rm -f "$target"; ln -s "$store/hm/fish/10-core.fish" "$target"
out=$(run_sync GS_HM_STORE_PREFIX="$store") || fail 'gs sync failed with Home Manager' "$out"
grep -q "^home-manager|switch --flake $dot#jbarbat" "$log" || fail 'home-manager switch not called with the flake' "$(cat "$log")"
grep -q "^home-manager|.* -b pre-gs" "$log" || fail 'home-manager switch without backup suffix' "$(cat "$log")"
[[ "$(readlink "$target")" == "$store/hm/fish/10-core.fish" ]] || fail 'Home Manager link was replaced' "$(ls -la "$target")"
out=$(run_sync GS_HM_STORE_PREFIX="$store" GS_HM_CONFIG=custom) || fail 'gs sync failed with GS_HM_CONFIG' "$out"
grep -q "^home-manager|switch --flake $dot#custom -b pre-gs" "$log" || fail 'GS_HM_CONFIG ignored' "$(cat "$log")"

echo "gs sync checks passed"
Loading