Repair theme symlinks the state-move migration left dangling (#6717)

* Repair theme symlinks the state-move migration left dangling

1781043107.sh re-linked legacy theme symlinks whose targets were stored
with a literal "~/.config/omarchy/current/..." string. The replacement
used the same literal tilde, which the filesystem never expands inside a
symlink target, so btop, Helix, and VS Code/Cursor lost their theme and
the migration reported success anyway.

Fix the source migration to relink through the already-defined
$current_state_dir variable, and add a follow-up migration that repairs
the links the applied version left dangling — matching the existing
1785002349.sh pattern, so it is idempotent and leaves custom links alone.

Co-Authored-By: Claude <noreply@anthropic.com>

* Only repair theme symlinks that could never have worked

The repair matched any target containing omarchy/current, so a working link
into a user's own dotfiles was rewritten to the state directory and their
setup was lost. Claim a link only when its target starts with a literal "~/",
which the filesystem never expands, and names this exact theme file: that is
what 1781043107.sh wrote, and no working link can look like it. A dangling
target is not enough on its own, since a dotfiles repo may just be unmounted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Clean up every scratch directory the CLI suite creates

Five of the eight mktemp directories were never registered with the exit
trap, so each run left them behind in /tmp. Route them all through a helper
that records them for cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the theme symlink migration tests

A migration runs once on each machine and is then inert, but a test for it
sits in the suite forever. Now that the repair behaves correctly, keep the
migration and let it go untested rather than grow the suite permanently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
This commit is contained in:
Shrijit Srivastav
2026-08-11 22:47:07 +02:00
committed by GitHub
co-authored by Claude Opus 5 David Heinemeier Hansson
parent 96a50cf65e
commit 1bb6600c76
3 changed files with 77 additions and 13 deletions
+3 -1
View File
@@ -99,8 +99,10 @@ relink_current_symlink() {
ln -sfn "$current_state_dir/$suffix" "$link" ln -sfn "$current_state_dir/$suffix" "$link"
;; ;;
"~/.config/omarchy/current/"*) "~/.config/omarchy/current/"*)
# The filesystem never expands a literal ~ in a symlink target, so keep
# $HOME out of the quoted string and let the shell expand it instead.
suffix=${target#"~/.config/omarchy/current/"} suffix=${target#"~/.config/omarchy/current/"}
ln -sfn "~/.local/state/omarchy/current/$suffix" "$link" ln -sfn "$current_state_dir/$suffix" "$link"
;; ;;
esac esac
} }
+51
View File
@@ -0,0 +1,51 @@
echo "Repair theme symlinks the state-move migration left dangling"
# 1781043107.sh re-linked legacy theme symlinks whose targets were stored as a
# literal "~/.config/omarchy/current/..." string. The replacement used the same
# literal tilde, which the filesystem never expands inside a symlink target, so
# btop, Helix, and VS Code/Cursor would have lost their theme while the
# migration reported success anyway. That migration is already marked applied
# everywhere it ran, so this one repairs any link it left dangling.
#
# No shipped Omarchy code ever wrote these six targets with a literal tilde, so
# this is expected to be a no-op almost everywhere, and it stays deliberately
# narrow to keep it that way. Only a target that starts with a literal "~/" is
# repaired: the filesystem never expands one, so such a link cannot ever have
# worked, while a custom link into a dotfiles repo is a real path even when that
# repo happens to be unmounted right now.
relink_if_dangling() {
local link="$1"
local expected_target="$2"
local target
[[ -L $link ]] || return 0
target=$(readlink "$link") || return 0
# Already pointing at the state directory.
[[ $target == "$expected_target" ]] && return 0
# Still resolves, so it is a working link we have no business rewriting.
[[ -e $link ]] && return 0
# Only an unexpandable literal-tilde target naming this theme file is ours.
case "$target" in
"~/"*/omarchy/current/theme/"${expected_target##*/}") ;;
*) return 0 ;;
esac
ln -sfn "$expected_target" "$link"
}
current_state_dir="$HOME/.local/state/omarchy/current"
relink_if_dangling "$HOME/.config/btop/themes/current.theme" \
"$current_state_dir/theme/btop.theme"
relink_if_dangling "$HOME/.config/helix/themes/omarchy.toml" \
"$current_state_dir/theme/helix.toml"
for vscode_dir in "$HOME/.vscode" "$HOME/.vscode-insiders" "$HOME/.vscode-oss" "$HOME/.cursor"; do
relink_if_dangling "$vscode_dir/extensions/omarchy-theme/themes/omarchy-color-theme.json" \
"$current_state_dir/theme/vscode-theme.json"
done
+23 -12
View File
@@ -4,12 +4,19 @@ set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
CLI="$ROOT/bin/omarchy" CLI="$ROOT/bin/omarchy"
TMPDIR="" TMPDIRS=()
PI_TMPDIR=""
THEME_TMPDIR=""
export PATH="$ROOT/bin:$PATH" export PATH="$ROOT/bin:$PATH"
# Assigns a fresh temporary directory to the named variable and registers it for
# removal on exit, so every scratch directory this suite makes is cleaned up.
make_tmpdir() {
local -n dir_ref="$1"
dir_ref=$(mktemp -d)
TMPDIRS+=("$dir_ref")
}
pass() { pass() {
printf 'ok - %s\n' "$1" printf 'ok - %s\n' "$1"
} }
@@ -34,9 +41,13 @@ assert_output_contains() {
} }
cleanup() { cleanup() {
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR" local dir
[[ -n $PI_TMPDIR && -d $PI_TMPDIR ]] && rm -rf "$PI_TMPDIR"
[[ -n $THEME_TMPDIR && -d $THEME_TMPDIR ]] && rm -rf "$THEME_TMPDIR" for dir in ${TMPDIRS[@]+"${TMPDIRS[@]}"}; do
[[ -d $dir ]] && rm -rf "$dir"
done
return 0
} }
trap cleanup EXIT trap cleanup EXIT
@@ -236,7 +247,7 @@ pass "safe dispatch works for font list"
"$CLI" font current >/dev/null "$CLI" font current >/dev/null
pass "safe dispatch works for font current" pass "safe dispatch works for font current"
PI_TMPDIR=$(mktemp -d) make_tmpdir PI_TMPDIR
NEXT_THEME="$PI_TMPDIR/.local/state/omarchy/current/next-theme" NEXT_THEME="$PI_TMPDIR/.local/state/omarchy/current/next-theme"
CURRENT_THEME="$PI_TMPDIR/.local/state/omarchy/current/theme" CURRENT_THEME="$PI_TMPDIR/.local/state/omarchy/current/theme"
USER_THEMED="$PI_TMPDIR/.config/omarchy/themed" USER_THEMED="$PI_TMPDIR/.config/omarchy/themed"
@@ -499,7 +510,7 @@ HOME="$PI_TMPDIR" CLAUDE_CONFIG_DIR="$CLAUDE_TEST_CONFIG" "$ROOT/bin/omarchy-the
jq -e '.theme == "custom:omarchy" and .model == "keep-me"' "$CLAUDE_TEST_CONFIG/settings.json" >/dev/null jq -e '.theme == "custom:omarchy" and .model == "keep-me"' "$CLAUDE_TEST_CONFIG/settings.json" >/dev/null
pass "claude theme activation preserves configured settings" pass "claude theme activation preserves configured settings"
THEME_TMPDIR=$(mktemp -d) make_tmpdir THEME_TMPDIR
OMARCHY_PATH="$ROOT" HOME="$THEME_TMPDIR" OMARCHY_THEME_HEADLESS=1 "$ROOT/bin/omarchy-theme-set" "Tokyo Night" OMARCHY_PATH="$ROOT" HOME="$THEME_TMPDIR" OMARCHY_THEME_HEADLESS=1 "$ROOT/bin/omarchy-theme-set" "Tokyo Night"
background_path=$(readlink -f "$THEME_TMPDIR/.local/state/omarchy/current/background" 2>/dev/null || true) background_path=$(readlink -f "$THEME_TMPDIR/.local/state/omarchy/current/background" 2>/dev/null || true)
expected_background="$THEME_TMPDIR/.local/state/omarchy/current/theme/backgrounds/0-swirl-buck.jpg" expected_background="$THEME_TMPDIR/.local/state/omarchy/current/theme/backgrounds/0-swirl-buck.jpg"
@@ -508,7 +519,7 @@ expected_background="$THEME_TMPDIR/.local/state/omarchy/current/theme/background
[[ ! -e $THEME_TMPDIR/.config/omarchy/current ]] || fail "headless theme set avoids config current state" [[ ! -e $THEME_TMPDIR/.config/omarchy/current ]] || fail "headless theme set avoids config current state"
pass "headless theme set creates current background symlink" pass "headless theme set creates current background symlink"
MIGRATION_TMPDIR=$(mktemp -d) make_tmpdir MIGRATION_TMPDIR
mkdir -p \ mkdir -p \
"$MIGRATION_TMPDIR/.config/omarchy/current/theme" \ "$MIGRATION_TMPDIR/.config/omarchy/current/theme" \
"$MIGRATION_TMPDIR/.config/alacritty" \ "$MIGRATION_TMPDIR/.config/alacritty" \
@@ -536,7 +547,7 @@ grep -Fq '/.local/state/?.lua;' "$MIGRATION_TMPDIR/.config/hypr/hyprland.lua" ||
[[ $(readlink "$MIGRATION_TMPDIR/.config/btop/themes/current.theme") == "$MIGRATION_TMPDIR/.local/state/omarchy/current/theme/btop.theme" ]] || fail "current theme migration updates btop symlink" [[ $(readlink "$MIGRATION_TMPDIR/.config/btop/themes/current.theme") == "$MIGRATION_TMPDIR/.local/state/omarchy/current/theme/btop.theme" ]] || fail "current theme migration updates btop symlink"
pass "current theme migration moves state and rewrites shipped references" pass "current theme migration moves state and rewrites shipped references"
NVIM_MIGRATION_TMPDIR=$(mktemp -d) make_tmpdir NVIM_MIGRATION_TMPDIR
nvim_theme_link="$NVIM_MIGRATION_TMPDIR/.config/nvim/lua/plugins/theme.lua" nvim_theme_link="$NVIM_MIGRATION_TMPDIR/.config/nvim/lua/plugins/theme.lua"
nvim_expected_target="../../../../.local/state/omarchy/current/theme/neovim.lua" nvim_expected_target="../../../../.local/state/omarchy/current/theme/neovim.lua"
nvim_expected_resolved_target="$NVIM_MIGRATION_TMPDIR/.local/state/omarchy/current/theme/neovim.lua" nvim_expected_resolved_target="$NVIM_MIGRATION_TMPDIR/.local/state/omarchy/current/theme/neovim.lua"
@@ -579,7 +590,7 @@ HOME="$NVIM_MIGRATION_TMPDIR" bash -euo pipefail "$ROOT/migrations/1785002349.sh
[[ $(readlink "$nvim_theme_link") == "../../../../custom-theme.lua" ]] || fail "nvim theme repair migration leaves custom symlinks alone" [[ $(readlink "$nvim_theme_link") == "../../../../custom-theme.lua" ]] || fail "nvim theme repair migration leaves custom symlinks alone"
pass "nvim theme repair migration relinks every legacy spelling" pass "nvim theme repair migration relinks every legacy spelling"
HYPR_MIGRATION_TMPDIR=$(mktemp -d) make_tmpdir HYPR_MIGRATION_TMPDIR
mkdir -p "$HYPR_MIGRATION_TMPDIR/.config/hypr" mkdir -p "$HYPR_MIGRATION_TMPDIR/.config/hypr"
cat >"$HYPR_MIGRATION_TMPDIR/.config/hypr/hyprland.lua" <<'LUA' cat >"$HYPR_MIGRATION_TMPDIR/.config/hypr/hyprland.lua" <<'LUA'
-- Learn how to configure Hyprland: https://wiki.hypr.land/Configuring/Start/ -- Learn how to configure Hyprland: https://wiki.hypr.land/Configuring/Start/
@@ -695,7 +706,7 @@ while IFS= read -r binary_path; do
done < <(find "$ROOT/bin" -maxdepth 1 -type f -executable -name 'omarchy-*' | sort) done < <(find "$ROOT/bin" -maxdepth 1 -type f -executable -name 'omarchy-*' | sort)
pass "all executable bins have slim self-documenting metadata" pass "all executable bins have slim self-documenting metadata"
TMPDIR=$(mktemp -d) make_tmpdir TMPDIR
ln -s "$CLI" "$TMPDIR/omarchy" ln -s "$CLI" "$TMPDIR/omarchy"
{ {