diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a10fd3..3ffa252 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,9 @@ jobs: - name: Check installer symlink safety run: ./scripts/check-install-symlinks.sh + - name: Check gs sync runs gentle-ai sync + run: bash scripts/test-gs-sync.sh + - name: Check Nix Home Manager preflight contracts run: bash scripts/test-nix-preflight.sh diff --git a/README.md b/README.md index 365de2e..b5d3f81 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ El preflight es de solo lectura y sin red: usa `--offline` y `--no-write-lock-fi | Zsh y Ghostty | Configuración actual, sin cambios | | `~/.config/fish/conf.d/99-local.fish` | Host; reservado para secretos/estado privado futuro, no creado ni gestionado por Nix | | `flake.nix` y `nix/home.nix` | Base inactiva de Home Manager | +| Linux | La configuración pura para Linux es `homeConfigurations.jbarbat-linux`: usa `x86_64-linux`, `/home/jbarbat` y declara solo `fish` y `starship` como runtime mínimo. | +| Fish compartido | Home Manager mapea cada archivo declarado de forma explícita; no gestiona el directorio completo, historial ni variables Fish. | El bootstrap queda explícitamente diferido a un work unit revisado. Después de instalar Nix por fuera de este repositorio, ese trabajo podrá generar el lock con: diff --git a/bin/gs b/bin/gs index 392b909..bf88509 100755 --- a/bin/gs +++ b/bin/gs @@ -29,8 +29,11 @@ gs - gentle-shell on main, plus one disposable worktree per PR gs review Check out someone else's PR to test or review it gs base Clean origin/main worktree, to compare failures gs run [pi args...] Run gentle-shell --link with this worktree as --package-root - gs sync Bring gentle-shell, gentle-ai and engram to main and check - compatibility (workshop: local clones; elsewhere: GitHub) + gs sync Bring gentle-shell, gentle-ai and engram to main, run + gentle-ai sync for managed configs, and check compatibility + (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) 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) @@ -342,6 +345,22 @@ cmd_sync() { ln -sfn "$ai_bin" "$HOME/.local/bin/gentle-ai" ln -sfn "$bin/engram" "$HOME/.local/bin/engram" + # New binaries are not enough: gentle-ai sync materializes the managed configs + # (agents, skills, plugins, engram instructions) every runtime reads. + # GS_SYNC_AGENTS scopes runtimes per host (local env); empty = gentle-ai default. + if [ "${GS_SKIP_AI_SYNC:-}" = 1 ]; then + printf ' %-34s skipped (GS_SKIP_AI_SYNC=1)\n' 'gentle-ai sync' + else + local ai_sync=(sync) + [ -n "${GS_SYNC_AGENTS:-}" ] && ai_sync+=(--agents "$GS_SYNC_AGENTS") + run_quiet 'gentle-ai sync' "$ai_bin" "${ai_sync[@]}" + fi + # 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 + [ -e "$name" ] && printf 'WARN: legacy upgrade script %s; use gs sync only and remove it\n' "$name" + done + # Versions after the sync decide the closing message: restart only on change. local shell_after ai_after engram_after changed shell_after=$(shell_version); ai_after=$(ai_version); engram_after=$(engram_version) diff --git a/fish/conf.d/10-core.fish b/fish/conf.d/10-core.fish index ea1bef7..1392862 100644 --- a/fish/conf.d/10-core.fish +++ b/fish/conf.d/10-core.fish @@ -75,6 +75,12 @@ else alias la 'ls -lah -A' end +# gentle-ai's managed launchers (OpenCode background subagents) must win on PATH; +# gentle-ai cannot persist PATH for fish login shells by itself. +if test -d "$HOME/.gentle-ai/bin" + fish_add_path --path --move "$HOME/.gentle-ai/bin" +end + if status is-interactive; and command -q starship starship init fish | source end diff --git a/fish/conf.d/99-local.fish.example b/fish/conf.d/99-local.fish.example index 1e46392..15f6c5c 100644 --- a/fish/conf.d/99-local.fish.example +++ b/fish/conf.d/99-local.fish.example @@ -8,3 +8,7 @@ # set -gx GIT_WORKDEV_EMAIL "you@company.invalid" # set -gx GIT_PERSONALDEV_NAME "Your personal name" # set -gx GIT_PERSONALDEV_EMAIL "you@personal.invalid" +# gs sync runs gentle-ai sync after updating binaries. Scope it to this host's runtimes: +# set -gx GS_SYNC_AGENTS claude-code,opencode,pi +# Binaries only, no managed-config sync: +# set -gx GS_SKIP_AI_SYNC 1 diff --git a/fish/functions/gbarbatdev.fish b/fish/functions/gbarbatdev.fish new file mode 100644 index 0000000..77d5f31 --- /dev/null +++ b/fish/functions/gbarbatdev.fish @@ -0,0 +1,54 @@ +# Open an isolated Ghostty instance with the Barbatdev profile. Usage: gbarbatdev [ghostty args...] +function gbarbatdev --description 'Open Ghostty with the Barbatdev profile' + if not command -q open + printf 'gbarbatdev: macOS open command is not available\n' >&2 + return 127 + end + + set -l ghostty /Applications/Ghostty.app/Contents/MacOS/ghostty + if not test -x "$ghostty" + printf 'gbarbatdev: Ghostty.app is not installed in /Applications\n' >&2 + return 1 + end + + set -l base "$HOME/.config/ghostty/config" + set -l profile "$HOME/.config/ghostty/profiles/barbatdev.conf" + if not test -r "$base" + printf 'gbarbatdev: missing readable config: %s\n' "$base" >&2 + return 1 + end + if not test -r "$profile" + printf 'gbarbatdev: missing readable config: %s\n' "$profile" >&2 + return 1 + end + + set -l tmp_dir (command mktemp -d) + set -l mktemp_status $status + if test $mktemp_status -ne 0 + return $mktemp_status + end + if test -z "$tmp_dir" + return 1 + end + + set -l config "$tmp_dir/config.ghostty" + printf 'config-file = "%s"\nconfig-file = "%s"\n' "$base" "$profile" > "$config" + set -l compose_status $status + if test $compose_status -ne 0 + command rm -rf -- "$tmp_dir" + return $compose_status + end + + "$ghostty" +validate-config --config-file="$config" + set -l validate_status $status + if test $validate_status -ne 0 + printf 'gbarbatdev: composed Ghostty config is invalid\n' >&2 + command rm -rf -- "$tmp_dir" + return $validate_status + end + + open -W -na Ghostty --args --config-default-files=false --config-file="$config" --working-directory="$PWD" $argv + set -l ghostty_status $status + command rm -rf -- "$tmp_dir" + return $ghostty_status +end diff --git a/fish/functions/grefactoria.fish b/fish/functions/grefactoria.fish new file mode 100644 index 0000000..905a37b --- /dev/null +++ b/fish/functions/grefactoria.fish @@ -0,0 +1,54 @@ +# Open an isolated Ghostty instance with the RefactorIA profile. Usage: grefactoria [ghostty args...] +function grefactoria --description 'Open Ghostty with the RefactorIA profile' + if not command -q open + printf 'grefactoria: macOS open command is not available\n' >&2 + return 127 + end + + set -l ghostty /Applications/Ghostty.app/Contents/MacOS/ghostty + if not test -x "$ghostty" + printf 'grefactoria: Ghostty.app is not installed in /Applications\n' >&2 + return 1 + end + + set -l base "$HOME/.config/ghostty/config" + set -l profile "$HOME/.config/ghostty/profiles/refactoria.conf" + if not test -r "$base" + printf 'grefactoria: missing readable config: %s\n' "$base" >&2 + return 1 + end + if not test -r "$profile" + printf 'grefactoria: missing readable config: %s\n' "$profile" >&2 + return 1 + end + + set -l tmp_dir (command mktemp -d) + set -l mktemp_status $status + if test $mktemp_status -ne 0 + return $mktemp_status + end + if test -z "$tmp_dir" + return 1 + end + + set -l config "$tmp_dir/config.ghostty" + printf 'config-file = "%s"\nconfig-file = "%s"\n' "$base" "$profile" > "$config" + set -l compose_status $status + if test $compose_status -ne 0 + command rm -rf -- "$tmp_dir" + return $compose_status + end + + "$ghostty" +validate-config --config-file="$config" + set -l validate_status $status + if test $validate_status -ne 0 + printf 'grefactoria: composed Ghostty config is invalid\n' >&2 + command rm -rf -- "$tmp_dir" + return $validate_status + end + + open -W -na Ghostty --args --config-default-files=false --config-file="$config" --working-directory="$PWD" $argv + set -l ghostty_status $status + command rm -rf -- "$tmp_dir" + return $ghostty_status +end diff --git a/fish/functions/hbarbatdev.fish b/fish/functions/hbarbatdev.fish new file mode 100644 index 0000000..737c1ba --- /dev/null +++ b/fish/functions/hbarbatdev.fish @@ -0,0 +1,48 @@ +# Run Herdr with the Barbatdev profile. Usage: hbarbatdev [herdr args...] +function hbarbatdev --description 'Run Herdr with the Barbatdev profile' + if set -q HERDR_ENV + printf 'hbarbatdev: launch this preview from an external Fish shell or window, not from within Herdr\n' >&2 + return 1 + end + + if not command -q herdr + printf 'hbarbatdev: herdr is not available\n' >&2 + return 127 + end + + set -l base "$HOME/.config/herdr/config-base.toml" + set -l profile "$HOME/.config/herdr/profiles/barbatdev.toml" + if not test -r "$base" + printf 'hbarbatdev: missing readable config: %s\n' "$base" >&2 + return 1 + end + if not test -r "$profile" + printf 'hbarbatdev: missing readable config: %s\n' "$profile" >&2 + return 1 + end + + set -l tmp_dir (command mktemp -d) + set -l mktemp_status $status + if test $mktemp_status -ne 0 + return $mktemp_status + end + if test -z "$tmp_dir" + return 1 + end + + set -l config "$tmp_dir/config.toml" + set -l server_socket "$tmp_dir/herdr.sock" + set -l client_socket "$tmp_dir/herdr-client.sock" + + command cat -- "$base" "$profile" > "$config" + set -l cat_status $status + if test $cat_status -ne 0 + command rm -rf -- "$tmp_dir" + return $cat_status + end + + env HERDR_CONFIG_PATH="$config" HERDR_SOCKET_PATH="$server_socket" HERDR_CLIENT_SOCKET_PATH="$client_socket" herdr --no-session $argv + set -l herdr_status $status + command rm -rf -- "$tmp_dir" + return $herdr_status +end diff --git a/fish/functions/hrefactoria.fish b/fish/functions/hrefactoria.fish new file mode 100644 index 0000000..4375135 --- /dev/null +++ b/fish/functions/hrefactoria.fish @@ -0,0 +1,48 @@ +# Run Herdr with the RefactorIA profile. Usage: hrefactoria [herdr args...] +function hrefactoria --description 'Run Herdr with the RefactorIA profile' + if set -q HERDR_ENV + printf 'hrefactoria: launch this preview from an external Fish shell or window, not from within Herdr\n' >&2 + return 1 + end + + if not command -q herdr + printf 'hrefactoria: herdr is not available\n' >&2 + return 127 + end + + set -l base "$HOME/.config/herdr/config-base.toml" + set -l profile "$HOME/.config/herdr/profiles/refactoria.toml" + if not test -r "$base" + printf 'hrefactoria: missing readable config: %s\n' "$base" >&2 + return 1 + end + if not test -r "$profile" + printf 'hrefactoria: missing readable config: %s\n' "$profile" >&2 + return 1 + end + + set -l tmp_dir (command mktemp -d) + set -l mktemp_status $status + if test $mktemp_status -ne 0 + return $mktemp_status + end + if test -z "$tmp_dir" + return 1 + end + + set -l config "$tmp_dir/config.toml" + set -l server_socket "$tmp_dir/herdr.sock" + set -l client_socket "$tmp_dir/herdr-client.sock" + + command cat -- "$base" "$profile" > "$config" + set -l cat_status $status + if test $cat_status -ne 0 + command rm -rf -- "$tmp_dir" + return $cat_status + end + + env HERDR_CONFIG_PATH="$config" HERDR_SOCKET_PATH="$server_socket" HERDR_CLIENT_SOCKET_PATH="$client_socket" herdr --no-session $argv + set -l herdr_status $status + command rm -rf -- "$tmp_dir" + return $herdr_status +end diff --git a/fish/tests/w7-gentle-ai-path.fish b/fish/tests/w7-gentle-ai-path.fish new file mode 100644 index 0000000..37cba5d --- /dev/null +++ b/fish/tests/w7-gentle-ai-path.fish @@ -0,0 +1,28 @@ +#!/usr/bin/env fish +# gentle-ai's managed launcher dir (~/.gentle-ai/bin) must lead PATH when present, +# because gentle-ai cannot persist PATH for fish login shells itself. + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -l core "$repo_root/conf.d/10-core.fish" +set -l fish_bin (status fish-path) +set -l ws (mktemp -d) + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +set -l probe 'source $argv[1]; string join "|" $PATH' + +mkdir -p "$ws/with/.gentle-ai/bin" "$ws/without" +set -l with (env -i HOME="$ws/with" PATH=/usr/bin:/bin "$fish_bin" --no-config -c "$probe" "$core") +string match -q "$ws/with/.gentle-ai/bin|*" -- "$with"; or fail "launcher dir not first in PATH: $with" + +set -l twice (env -i HOME="$ws/with" PATH="$ws/with/.gentle-ai/bin:/usr/bin:/bin" "$fish_bin" --no-config -c "$probe" "$core") +test (count (string match -a -- "$ws/with/.gentle-ai/bin" (string split '|' -- "$twice"))) -eq 1; or fail "launcher dir duplicated: $twice" + +set -l without (env -i HOME="$ws/without" PATH=/usr/bin:/bin "$fish_bin" --no-config -c "$probe" "$core") +string match -q '*gentle-ai*' -- "$without"; and fail "missing launcher dir added to PATH: $without" + +rm -rf -- "$ws" +printf 'gentle-ai path checks passed\n' diff --git a/fish/themes/barbatdev.fish b/fish/themes/barbatdev.fish new file mode 100644 index 0000000..bbd9feb --- /dev/null +++ b/fish/themes/barbatdev.fish @@ -0,0 +1,41 @@ +# Barbatdev Fish colors. + +set -l base "#0B0B0A" +set -l panel "#171512" +set -l text "#F8F1E7" +set -l selection_text "#FFF7ED" +set -l accent "#F97316" +set -l active_orange "#FB923C" +set -l gold "#FBBF24" +set -l soft_orange "#FDBA74" +set -l bright_blue "#BAE6FD" +set -l error "#EF4444" +set -l muted "#B9AD9C" +set -l dim "#8C8173" +set -l selection "#7A3500" +set -l search_background "#3A1D0C" + +# Syntax highlighting colors. +set -g fish_color_normal $text +set -g fish_color_command --bold $accent +set -g fish_color_keyword --bold $active_orange +set -g fish_color_quote $gold +set -g fish_color_redirection $bright_blue +set -g fish_color_end $soft_orange +set -g fish_color_error --bold $error +set -g fish_color_param $soft_orange +set -g fish_color_comment $dim +set -g fish_color_selection --background=$selection $selection_text +set -g fish_color_search_match --background=$search_background $selection_text +set -g fish_color_operator $bright_blue +set -g fish_color_escape $active_orange +set -g fish_color_autosuggestion $muted +set -g fish_color_option $soft_orange + +# Completion pager colors. +set -g fish_pager_color_progress $dim +set -g fish_pager_color_prefix --bold $accent +set -g fish_pager_color_completion $text +set -g fish_pager_color_description $muted +set -g fish_pager_color_selected_background --background=$selection $selection_text +set -g fish_pager_color_secondary_background --background=$panel diff --git a/ghostty/profiles/barbatdev.conf b/ghostty/profiles/barbatdev.conf new file mode 100644 index 0000000..9ddeeb0 --- /dev/null +++ b/ghostty/profiles/barbatdev.conf @@ -0,0 +1,25 @@ +# Barbatdev terminal palette. Loaded after the shared Ghostty config. +background = #0B0B0A +foreground = #F8F1E7 +cursor-color = #F97316 +cursor-text = #0B0B0A +selection-background = #7C2D12 +selection-foreground = #F8F1E7 + +# Accessible ANSI colors for the graphite background. +palette = 0=#8B8378 +palette = 1=#F87171 +palette = 2=#34D399 +palette = 3=#FBBF24 +palette = 4=#60A5FA +palette = 5=#C084FC +palette = 6=#2DD4BF +palette = 7=#F8F1E7 +palette = 8=#B8AEA0 +palette = 9=#FCA5A5 +palette = 10=#6EE7B7 +palette = 11=#FCD34D +palette = 12=#93C5FD +palette = 13=#D8B4FE +palette = 14=#5EEAD4 +palette = 15=#FFFFFF diff --git a/ghostty/profiles/refactoria.conf b/ghostty/profiles/refactoria.conf new file mode 100644 index 0000000..3ad7e07 --- /dev/null +++ b/ghostty/profiles/refactoria.conf @@ -0,0 +1,27 @@ +# RefactorIA terminal palette. Loaded after the shared Ghostty config. +background = #0B0714 +foreground = #FFFFFF +cursor-color = #7127FF +cursor-text = #0B0714 +selection-background = #FFFFFF +selection-foreground = #0B0714 + +# ANSI semantic colors are adapted for contrast; they are not RefactorIA brand tokens. +palette = 0=#0B0714 +palette = 1=#FF6B8A +palette = 2=#58E6A8 +palette = 3=#FFC857 +palette = 4=#8CC8FF +palette = 5=#C7A6FF +palette = 6=#66E6E6 +palette = 7=#FFFFFF + +# Bright ANSI variants retain the same semantic roles. +palette = 8=#8F839D +palette = 9=#FF9CB0 +palette = 10=#8FF0C3 +palette = 11=#FFD98A +palette = 12=#B8DDFF +palette = 13=#E1CFFF +palette = 14=#A8F2F2 +palette = 15=#FFFFFF diff --git a/herdr/config-base.toml b/herdr/config-base.toml new file mode 100644 index 0000000..a6c9038 --- /dev/null +++ b/herdr/config-base.toml @@ -0,0 +1,15 @@ +# Shared Herdr settings managed by Gentleman.Dots. + +[keys] +# Match tmux/Zellij-style prefix muscle memory. +prefix = "ctrl+a" + +# Move between agents in the sidebar. +# Avoid Herdr's built-in prefix+shift+j/k pane-swap bindings. +previous_agent = "prefix+alt+k" +next_agent = "prefix+alt+j" + +# Jump straight to agent N (1..9) as listed in the sidebar. +# Use Ctrl after the prefix: Alt+number is owned by skhd/yabai spaces, +# and Shift+number did not reach Herdr reliably in the current terminal setup. +focus_agent = "prefix+ctrl+1..9" diff --git a/herdr/profiles/barbatdev.toml b/herdr/profiles/barbatdev.toml new file mode 100644 index 0000000..953c8fa --- /dev/null +++ b/herdr/profiles/barbatdev.toml @@ -0,0 +1,31 @@ +# Barbatdev/Cortex orange Herdr profile. + +[theme] +# One Dark supplies stable structural defaults outside Herdr's custom palette. +name = "one-dark" +auto_switch = false + +[theme.custom] +panel_bg = "#27231D" +surface0 = "#17130F" +surface1 = "#27231D" +surface_dim = "#11100E" +overlay0 = "#8E8375" +overlay1 = "#B9AD9C" +text = "#F8F1E7" +subtext0 = "#B9AD9C" +accent = "#F97316" +blue = "#60A5FA" +red = "#F87171" +green = "#34D399" +yellow = "#FBBF24" +mauve = "#A78BFA" +teal = "#2DD4BF" +peach = "#FB923C" + +[ui] +accent = "#F97316" +show_agent_labels_on_pane_borders = true + +[ui.toast] +delivery = "herdr" diff --git a/install.sh b/install.sh index 9ab406b..08c43b5 100755 --- a/install.sh +++ b/install.sh @@ -224,7 +224,10 @@ PY check_symlink_target "$DOTFILES/fish/functions/gbarbatdev.fish" "$HOME/.config/fish/functions/gbarbatdev.fish" check_symlink_target "$DOTFILES/fish/functions/grefactoria.fish" "$HOME/.config/fish/functions/grefactoria.fish" fi - check_symlink_target "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.json" + check_file "$DOTFILES/opencode/tui.json" + if [[ -L "$HOME/.config/opencode/tui.json" ]]; then + warn "opencode tui.json is a symlink; gentle-ai sync refuses it (install.sh converts it to a copy)" + fi check_symlink_target "$DOTFILES/opencode/themes" "$HOME/.config/opencode/themes" check_symlink_target "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" check_symlink_target "$DOTFILES/claude/statusline.sh" "$HOME/.claude/statusline.sh" @@ -500,7 +503,6 @@ backup_if_exists "$HOME/.config/starship.toml" backup_if_exists "$HOME/.config/herdr/config.toml" backup_if_exists "$HOME/.config/ghostty/config" backup_if_exists "$HOME/.config/herdr/config.toml" -backup_if_exists "$HOME/.config/opencode/tui.json" backup_if_exists "$HOME/.config/opencode/themes" backup_if_exists "$HOME/.tmux.conf" backup_if_exists "$HOME/.claude/statusline.sh" @@ -566,7 +568,10 @@ if [[ "$PLATFORM" == "Darwin" ]]; then create_symlink "$DOTFILES/fish/functions/gbarbatdev.fish" "$HOME/.config/fish/functions/gbarbatdev.fish" create_symlink "$DOTFILES/fish/functions/grefactoria.fish" "$HOME/.config/fish/functions/grefactoria.fish" fi -create_symlink "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.json" +# gentle-ai sync edits tui.json and refuses symlinks: seed a copy instead. +# shellcheck source=scripts/seed-file.sh +source "$DOTFILES/scripts/seed-file.sh" +seed_file "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.json" create_symlink "$DOTFILES/opencode/themes" "$HOME/.config/opencode/themes" create_symlink "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" run_or_plan "chmod +x $DOTFILES/claude/statusline.sh" chmod +x "$DOTFILES/claude/statusline.sh" diff --git a/local/env.zsh.example b/local/env.zsh.example index d759cc6..4766b42 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -48,3 +48,8 @@ # export CORTEX_BRAINS_OLLAMA_URL="http://:11434" # export CORTEX_BRAINS_PG_HOST="" # export CORTEX_BRAINS_PG_PORT="5432" + +# gs sync runs gentle-ai sync after updating binaries. Scope it to this host's runtimes: +# export GS_SYNC_AGENTS="claude-code,opencode,pi" +# Binaries only, no managed-config sync: +# export GS_SKIP_AI_SYNC=1 diff --git a/pi/themes/barbatdev.json b/pi/themes/barbatdev.json new file mode 100644 index 0000000..27473c4 --- /dev/null +++ b/pi/themes/barbatdev.json @@ -0,0 +1,94 @@ +{ + "$schema": "https://raw.githubusercontent.com/earendil-works/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", + "name": "barbatdev", + "vars": { + "bg": "#0B0B0A", + "bgPanel": "#171512", + "bgElement": "#211F1A", + "bgSubtle": "#171512", + "border": "#7C2D12", + "borderSubtle": "#3A2114", + "text": "#F8F1E7", + "muted": "#B9AD9C", + "mutedBright": "#D8CBB8", + "dim": "#8C8173", + "accent": "#F97316", + "deepOrange": "#C2410C", + "activeOrange": "#FB923C", + "gold": "#FBBF24", + "softOrange": "#FDBA74", + "blue": "#60A5FA", + "brightBlue": "#BAE6FD", + "successBlue": "#7DD3FC", + "error": "#EF4444", + "selection": "#3A1D0C", + "userBg": "#7A3500", + "userText": "#FFF7ED", + "toolPendingBg": "#171512", + "toolSuccessBg": "#2A1A0D", + "toolErrorBg": "#2A0E0E", + "infoBg": "#171512" + }, + "colors": { + "accent": "accent", + "border": "border", + "borderAccent": "activeOrange", + "borderMuted": "borderSubtle", + "success": "successBlue", + "error": "error", + "warning": "gold", + "muted": "muted", + "dim": "dim", + "text": "text", + "thinkingText": "muted", + "selectedBg": "selection", + "scrollbarThumb": "border", + "searchMatchBg": "deepOrange", + "searchMatchText": "text", + "userMessageBg": "userBg", + "userMessageText": "userText", + "customMessageBg": "bgSubtle", + "customMessageText": "text", + "customMessageLabel": "gold", + "toolPendingBg": "toolPendingBg", + "toolSuccessBg": "toolSuccessBg", + "toolErrorBg": "toolErrorBg", + "toolTitle": "gold", + "toolOutput": "text", + "mdHeading": "gold", + "mdLink": "brightBlue", + "mdLinkUrl": "mutedBright", + "mdCode": "gold", + "mdCodeBlock": "text", + "mdCodeBlockBorder": "mutedBright", + "mdQuote": "mutedBright", + "mdQuoteBorder": "gold", + "mdHr": "muted", + "mdListBullet": "gold", + "toolDiffAdded": "successBlue", + "toolDiffRemoved": "error", + "toolDiffContext": "muted", + "syntaxComment": "mutedBright", + "syntaxKeyword": "gold", + "syntaxFunction": "brightBlue", + "syntaxVariable": "text", + "syntaxString": "gold", + "syntaxNumber": "softOrange", + "syntaxType": "softOrange", + "syntaxOperator": "brightBlue", + "syntaxPunctuation": "mutedBright", + "thinkingOff": "dim", + "thinkingMinimal": "dim", + "thinkingLow": "muted", + "thinkingMedium": "blue", + "thinkingHigh": "gold", + "thinkingXhigh": "activeOrange", + "thinkingMax": "accent", + "bashMode": "gold" + }, + "export": { + "pageBg": "bg", + "cardBg": "bgElement", + "infoBg": "infoBg" + } +} diff --git a/scripts/check-install-symlinks.sh b/scripts/check-install-symlinks.sh index 606835b..103bf38 100755 --- a/scripts/check-install-symlinks.sh +++ b/scripts/check-install-symlinks.sh @@ -55,3 +55,37 @@ if backup_if_exists "$collision" >/dev/null 2>&1; then fi [[ "$(<"$collision")" == original ]] [[ "$(<"$collision.bak_test")" == existing ]] + +# seed_file: tool-owned configs (gentle-ai refuses symlinks) are copied once. +# shellcheck source=scripts/seed-file.sh +source "$repo_root/scripts/seed-file.sh" +seed_src="$tmp_dir/seed-src" +printf '%s\n' seed > "$seed_src" + +seed_absent="$tmp_dir/seed/absent.json" +seed_file "$seed_src" "$seed_absent" >/dev/null +[[ -f "$seed_absent" && ! -L "$seed_absent" && "$(<"$seed_absent")" == seed ]] + +seed_link="$tmp_dir/seed-link.json" +ln -s "$seed_src" "$seed_link" +seed_file "$seed_src" "$seed_link" >/dev/null +[[ -f "$seed_link" && ! -L "$seed_link" && "$(<"$seed_link")" == seed ]] +[[ "$(<"$seed_src")" == seed ]] + +seed_owned="$tmp_dir/seed-owned.json" +printf '%s\n' tool-edited > "$seed_owned" +seed_file "$seed_src" "$seed_owned" >/dev/null +[[ "$(<"$seed_owned")" == tool-edited ]] + +seed_foreign="$tmp_dir/seed-foreign.json" +ln -s "$old_target" "$seed_foreign" +seed_file "$seed_src" "$seed_foreign" >/dev/null +[[ -L "$seed_foreign" && "$(readlink "$seed_foreign")" == "$old_target" ]] + +DRY_RUN=true +seed_dry="$tmp_dir/seed-dry.json" +seed_file "$seed_src" "$seed_dry" >/dev/null +[[ ! -e "$seed_dry" ]] +DRY_RUN=false + +echo "install symlink checks passed" diff --git a/scripts/seed-file.sh b/scripts/seed-file.sh new file mode 100644 index 0000000..a57fe32 --- /dev/null +++ b/scripts/seed-file.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# seed_file SRC DST — install a config that another tool owns after first run. +# Some managers (gentle-ai sync) refuse to read or write symlinks, so these +# files are copied, never linked: +# - absent DST: copy SRC +# - DST is our old symlink to SRC: replace it with a regular copy +# - DST is a regular file: keep it; the owning tool may have edited it +# - DST is a symlink elsewhere: keep it and warn +seed_file() { + local src="$1" + local dst="$2" + + if [[ -L "$dst" && "$(readlink "$dst")" != "$src" ]]; then + echo " ! $dst is a foreign symlink; left untouched" >&2 + return 0 + fi + if [[ -e "$dst" && ! -L "$dst" ]]; then + echo " = $dst kept (tool-owned)" + return 0 + fi + if [[ "$DRY_RUN" == true ]]; then + echo " → Would seed $dst from $src" + return 0 + fi + mkdir -p "$(dirname "$dst")" + local tmp + tmp="$(mktemp "$dst.seed.XXXXXX")" + cp "$src" "$tmp" + # mv over a symlink replaces the link itself, never its target. + mv -f "$tmp" "$dst" + echo " ✓ $dst seeded from $src" +} diff --git a/scripts/test-gs-sync.sh b/scripts/test-gs-sync.sh new file mode 100644 index 0000000..067b58a --- /dev/null +++ b/scripts/test-gs-sync.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Run the real `gs sync` (consumer mode) with an isolated HOME and PATH shims: +# no network, no host installs. Checks that the binaries step is followed by +# `gentle-ai sync`, scoped by GS_SYNC_AGENTS and skippable with GS_SKIP_AI_SYNC. +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +ws="$(mktemp -d)" +trap 'rm -rf "$ws"' EXIT + +fail() { printf 'FAIL: %s\n%s\n' "$1" "${2:-}" >&2; exit 1; } + +home="$ws/home" +shims="$ws/shims" +log="$ws/calls.log" +mkdir -p "$home/.pi/gentle-ai" "$shims" + +# One shim script, dispatched by name; every call is logged. +cat >"$shims/_shim" <>"$log" +case "\$name \$1" in + "gentle-shell --version") echo "gentle-shell 4.2.0" ;; + "go install") + for arg in "\$@"; do case "\$arg" in *cmd/engram@*) + printf '#!/bin/sh\necho "engram 3.0.0"\n' >"\$GOBIN/engram"; chmod +x "\$GOBIN/engram" ;; + esac; done ;; + "gentle-ai --version") echo "gentle-ai 4.2.1" ;; + "gentle-ai sync") [ -n "\${FAKE_AI_SYNC_FAIL:-}" ] && { echo "Error: partial sync"; exit 1; }; echo "managed sync executed" ;; +esac +exit 0 +EOF +chmod +x "$shims/_shim" +for name in gentle-shell pi go; 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" + +run_sync() { + : >"$log" + env -i HOME="$home" PATH="$shims:/usr/bin:/bin" TERM=dumb "$@" \ + bash "$repo_root/bin/gs" sync 2>&1 +} +ai_sync_calls() { grep '^gentle-ai|sync' "$log" || true; } + +# 1. Default: gentle-ai sync runs once, unscoped, after the binaries step. +out=$(run_sync) || fail 'gs sync exited non-zero' "$out" +[[ "$(ai_sync_calls)" == 'gentle-ai|sync' ]] || fail 'expected one unscoped gentle-ai sync' "$(cat "$log")" +[[ "$out" == *'gentle-ai sync'*'ok'* ]] || fail 'missing gentle-ai sync status line' "$out" +engram_line=$(grep -n 'cmd/engram@main' "$log" | cut -d: -f1) +sync_line=$(grep -n '^gentle-ai|sync' "$log" | cut -d: -f1) +(( engram_line < sync_line )) || fail 'gentle-ai sync ran before the binaries step' "$(cat "$log")" + +# 2. GS_SYNC_AGENTS scopes the runtimes for this host. +out=$(run_sync GS_SYNC_AGENTS=claude-code,opencode,pi) || fail 'scoped gs sync exited non-zero' "$out" +[[ "$(ai_sync_calls)" == 'gentle-ai|sync --agents claude-code,opencode,pi' ]] || fail 'agents scope not forwarded' "$(cat "$log")" + +# 3. GS_SKIP_AI_SYNC=1 keeps the binaries-only behavior. +out=$(run_sync GS_SKIP_AI_SYNC=1) || fail 'skipped gs sync exited non-zero' "$out" +[[ -z "$(ai_sync_calls)" ]] || fail 'gentle-ai sync ran despite GS_SKIP_AI_SYNC=1' "$(cat "$log")" +[[ "$out" == *'gentle-ai sync'*'skipped (GS_SKIP_AI_SYNC=1)'* ]] || fail 'missing skip status line' "$out" + +# 4. A failing gentle-ai sync is reported with its output, and gs sync goes on. +out=$(run_sync FAKE_AI_SYNC_FAIL=1) || fail 'gs sync aborted on gentle-ai sync failure' "$out" +[[ "$out" == *'gentle-ai sync'*'FAILED'*'Error: partial sync'* ]] || fail 'failure output not surfaced' "$out" +[[ "$out" == *'versions:'* ]] || fail 'sync stopped before the closing report' "$out" + +# 5. Legacy upgrade scripts are flagged so only gs sync remains. +mkdir -p "$home/.local/bin"; printf '#!/bin/sh\n' >"$home/.local/bin/gentle-maintainer-upgrade" +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" + +echo "gs sync checks passed"