Skip to content

Reconcile main and pc/xps: pick a per-host strategy #3

@rdlu

Description

@rdlu

Status / Decisions (resolved)

# Question Answer
1 Which option? Option A (per-host stow overlay). Chezmoi (Option D) tracked separately as a parallel spike in #4.
2 Stale branches origin/xps, origin/alt, origin/master deleted. Local danklinux deleted (rejected dank niri experiment). pc/xps retained until migration extracts portable commits.
3 Waybar split Try the DRYer split (base + per-host fragment merged at setup). Fall back to whole-file overlay if it gets painful.
4 mpd Used on all machines → not host-specific. Belongs as a base systemd enable, not in the xps overlay.
5 yay vs paru Justfile standardizes on paru (CachyOS default — guaranteed present on fresh install). User keeps yay separately as muscle-memory for interactive use; optional install-yay recipe.
6 systemd *.target.wants/* symlinks Gitignore them. Repo tracks unit files (what exists); a services-enable justfile recipe tracks intent (what's enabled). Uses the proper systemctl enable API.
7 mise per-host catalog conf.d/ per-host overlay — base shared in terminal/, host-specific tools in hosts/<host>/dot-config/mise/conf.d/<host>.toml. Tool tables merge additively.

Open


Context

pc/xps has been the working branch on the XPS machine; main has continued to receive general updates. They've drifted and need reconciling. Before doing that, we should pick a per-host model so this doesn't keep happening.

Target universe: two CachyOS notebooks, daisy (AMD Ryzen) and xps (Intel i7). No Fedora, no WSL — those paths are obsolete.

Current state (snapshot)

  • Merge base: 3eab9cc
  • main is 8 commits ahead of the merge base
  • pc/xps is 5 commits ahead of the merge base
  • Net diff: 21 files changed, 396 insertions(+), 83 deletions(-)

Classifying the 5 pc/xps commits

Commit Subject Verdict
ec93a6d solaar: add [Install] section so it can be enabled Portable — fixes a missing section, belongs on main
aca17a9 lazy updt Lock-file churn — fold into next main update
617aab2 nvim: switch to claudecode, add fff.nvim Portable — belongs on main
5504ce0 tmux F12 toggle for nested sessions (+ fish ssh wrapper) Portable — belongs on main
837a18a xps mods Mixed — see below

Breaking down 837a18a ("xps mods"):

  • home/dot-local/share/applications/*.desktop (BoxySVG, JetBrains Toolbox, Tidewave, 3 WebApps, Zoom) → host-specific (apps installed only on XPS)
  • home/dot-local/share/applications/mimeinfo.cacheshouldn't be tracked at all (regenerated by update-desktop-database)
  • home/dot-local/share/applications/mimeapps.list (empty) → noise
  • niri/dot-config/systemd/user/default.target.wants/mpd.service, sockets.target.wants/mpd.socketshouldn't be tracked as symlinks. mpd is used on all machines; the enable step belongs in the base setup (justfile recipe), not as an xps-only artifact.
  • lazyvim/.../nvim-light/spell/pt.utf-8.splportable (Portuguese spellfile, useful anywhere)
  • niri/dot-config/waybar/config.jsonc → toggles thermal-zone: 8 and critical-threshold: 90genuinely host-specific (XPS exposes a different sensor index than daisy)
  • lazy-lock.json updates → lock-file churn

So the truly host-specific surface area on XPS is small: a handful of .desktop shortcuts and one waybar block. Everything else either belongs on main, belongs in the base setup, or shouldn't be in git.


Options (Option A chosen — kept for context)

Option A — Single main, per-host stow package ✅ chosen

Extend the existing GNU Stow workflow with a hosts/<hostname>/ package that gets stowed only on machines with that hostname. The base packages stay machine-agnostic.

This formalizes what's already happening informally — the stow and stow-daisy recipes in the justfile are de-facto per-host overlays, just smushed into the build system instead of the file tree.

Layout:
```
hosts/
xps/
home/dot-local/share/applications/ # XPS-only .desktop shortcuts
niri/dot-config/waybar/ # XPS waybar fragment (DRY split)
dot-config/mise/conf.d/xps.toml # XPS-only mise tools
daisy/
niri/dot-config/waybar/ # daisy waybar fragment (if any)
dot-config/mise/conf.d/daisy.toml # daisy-only mise tools
```

A single stow recipe stows base packages, then runs stow hosts/$(hostname) if that directory exists.

For waybar: split config.jsonc into a base file + per-host fragment, concatenated at setup time (or via waybar's include directive if available — needs verification). Fall back to whole-file override if splitting proves painful.

Pros: one branch, the existing stow model just grows one level, adding a new machine = mkdir hosts/<name>/ and stow.
Cons: a few configs (waybar) don't have native include — need a small build step.

Option B — Single main, runtime detection in configs

Keep one tree, but configs detect host at load time (fish conf.d/host-$(hostname).fish, tmux if-shell, etc.).

Pros: zero per-host bookkeeping at install time. Cons: only works for configs that support conditional/include syntax; clutters base configs for everyone.

Option C — Keep pc/xps as a long-lived branch, codify the workflow

Status quo with rules. Pros: minimal refactor. Cons: pure discipline; this is exactly the workflow that already drifted.

Option D — Migrate to chezmoi

Tracked separately in #4 as a parallel spike. Decision between A and D will happen after that spike runs.


Migration plan (Option A) — what

High-level: cherry-pick portable commits, untrack generated files, formalize host overlay, refactor justfile, then validate per-machine.

  1. Promote portable commits to main (cherry-pick): ec93a6d (solaar Install), 617aab2 (claudecode + fff.nvim), 5504ce0 (tmux F12 + fish ssh wrapper), and the pt.utf-8.spl spellfile from 837a18a. Roll lock-file updates into the next normal lazy updt on main.
  2. Stop tracking generated/state files and add to .gitignore:
    • home/dot-local/share/applications/mimeinfo.cache
    • **/systemd/user/*.target.wants/
    • empty mimeapps.list
  3. Add services-enable justfile recipe — declarative "what's enabled" using systemctl --user enable --now mpd.socket mpd.service syncthing.service solaar.service .... Idempotent. Replaces the tracked *.target.wants/ symlinks.
  4. Create hosts/xps/ with the remaining XPS-only bits (.desktop shortcuts + waybar thermal fragment + xps-only mise conf.d/ file).
  5. Refactor the stow recipes in the justfile (see Justfile section) — collapse stow / stow-daisy / stow-check into a single host-aware recipe.
  6. Audit and split mise [tools] table between shared base and per-host conf.d/ files (see mise section).
  7. Delete pc/xps once steps 1–6 are merged on main.

Execution plan — both machines (the runbook)

This is the per-machine runbook. The order matters: Phase 0 (prep on daisy) → Phase 1 (validate on daisy) → Phase 2 (validate on xps) → Phase 3 (merge + cleanup). Don't migrate both machines in parallel — daisy's first apply is also the test of the new tooling, and if anything is broken you want to find out before xps repeats the mistake.

Work happens on a migration branch; main stays untouched until both machines have validated.

Phase 0 — Prep (on daisy, in the dotfiles repo, no machine state changes)

All git/file work — nothing touches ~/.config yet. This produces a migration branch that can be reviewed on GitHub before any machine state changes.

  • Create migration branch from main
  • Cherry-pick portable commits from pc/xps: ec93a6d, 617aab2, 5504ce0
  • Extract just the spellfile from 837a18a (the rest of that commit's contents are split between hosts/xps/ and .gitignore):
    ```bash
    git checkout pc/xps -- lazyvim/dot-config/nvim-light/spell/pt.utf-8.spl
    git commit -m "lazyvim: add Portuguese spellfile (from pc/xps)"
    ```
  • Add .gitignore entries for generated/state files:
    ```
    home/dot-local/share/applications/mimeinfo.cache
    home/dot-local/share/applications/mimeapps.list
    **/systemd/user/*.target.wants/
    ```
  • Create hosts/xps/ skeleton:
    • hosts/xps/home/dot-local/share/applications/ — relocate WebApp-OctoChat4985.desktop, WebApp-WeatherMerryChrome4325.desktop, WebApp-WeatherMerrySky1749.desktop, com.boxy_svg.BoxySVG.desktop, jetbrains-toolbox.desktop, tidewave.desktop, Zoom-*.desktop (sourced from pc/xps)
    • hosts/xps/niri/dot-config/waybar/temperature.jsonc — XPS thermal fragment (thermal-zone: 8, critical-threshold: 90)
    • hosts/xps/dot-config/mise/conf.d/xps.toml — empty placeholder; populated during Phase 2 audit
  • Create hosts/daisy/ skeleton:
    • hosts/daisy/niri/dot-config/waybar/temperature.jsonc — daisy thermal fragment (critical-threshold: 80)
    • hosts/daisy/dot-config/mise/conf.d/daisy.toml — empty placeholder
  • Refactor niri/dot-config/waybar/config.jsonc to remove the inline temperature block and reference the per-host fragment (mechanism TBD: waybar include if supported; otherwise concat at setup time via a justfile recipe)
  • Refactor justfile:
    • Single host-aware stow recipe: stow base packages with -R (restow), then stow hosts/$(hostname)/ if present
    • Add services-enable: systemctl --user enable --now mpd.socket mpd.service syncthing.service solaar.service swayidle.service waybar.service wpaperd.service mako.service
    • Add update: paru -Syu, mise upgrade, fisher update, ya pkg upgrade, nvim --headless \"+Lazy! sync\" +qa
    • Add doctor: check fish/tmux/stow/mise installed, $SHELL is fish, expected services enabled, mise ls resolves, key symlinks live
    • Add set shell := [\"bash\", \"-uc\"] at top
    • Swap every yay -Sparu -S
    • Idempotency: gate git clone .../tpm on [ -d ~/.tmux/plugins/tpm ], gate chsh on $SHELL check
    • Remove redundant per-app stow calls (stow runs once at the end, not in fish-shell/kitty-terminal/yazi-file-manager)
    • Drop Fedora bits, delete setup/fedora-base.fish, setup/archlinux-wsl2.fish, and the gnupg/ directory
    • Append mise install to dev-setup
  • Push migration to origin
  • Verify on GitHub: PR diff against main looks right; no surprises

Phase 1 — Validate on daisy

Daisy first because instant feedback, and current state is closer to main than pc/xps is.

  • Backup:
    ```bash
    tar czf ~/dotfiles-backup-$(date +%F-%H%M).tgz ~/.config ~/.tmux.conf ~/.local/share/applications ~/.tmux 2>/dev/null
    ```
  • Verify no uncommitted changes: cd ~/.dotfiles && git status should be clean
  • Pull and check out the migration branch:
    ```bash
    cd ~/.dotfiles && git fetch origin && git checkout migration
    ```
  • Restow with the new layout (host-aware recipe):
    ```bash
    just stow
    ```
  • Apply service state:
    ```bash
    just services-enable
    ```
  • Pull any new mise tools (additive, doesn't remove):
    ```bash
    mise install
    ```
  • Reload niri-related services and shell:
    ```bash
    just wpaper-reload mako-reload waybar-reload swayidle-reload
    exec fish
    ```
  • Smoke test:
    • tmux F12 toggles status bar to red and disables prefix locally
    • ssh somehost (any host) flips tmux into REMOTE mode for the duration of the session
    • waybar temperature widget shows daisy's thermal zone (the non-XPS branch)
    • systemctl --user is-active mpd.serviceactive
    • mise ls shows the same tool set as before (plus any new ones from the merged config)
    • claudecode / fff.nvim work in nvim
  • just doctor — should be all green
  • If anything's broken: git checkout main && just stow (old recipe still works pre-merge), restore from tarball if needed
  • Commit any tweaks discovered during validation; push to migration

Phase 2 — Validate on xps

The order on xps matters because of the dangling-symlink trap. Do this from a TTY (Ctrl+Alt+F2) so a niri/waybar restart mid-stream doesn't lose your terminal.

  • Switch to TTY (Ctrl+Alt+F2) and log in
  • Backup:
    ```bash
    tar czf ~/dotfiles-backup-$(date +%F-%H%M).tgz ~/.config ~/.tmux.conf ~/.local/share/applications ~/.tmux 2>/dev/null
    ```
  • Verify on pc/xps with no uncommitted changes:
    ```bash
    cd ~/.dotfiles && git status
    ```
    Stash or commit anything important.
  • CRITICAL: unstow with the OLD package list while the source files for pc/xps are still in the working tree. Skipping this leaves dangling symlinks for mimeinfo.cache and the *.target.wants/ symlinks once the migration branch removes them:
    ```bash
    cd ~/.dotfiles
    stow --dotfiles -D home idea git scripts terminal lazyvim vim systemd niri
    ```
  • Verify no dotfile-owned dangling symlinks remain in ~/.config:
    ```bash
    find ~/.config ~/.local/share/applications ~/.tmux.conf -xtype l 2>/dev/null | grep dotfiles
    ```
    (No output expected. If any show up, rm them — they're orphans from the old layout.)
  • Switch to migration branch:
    ```bash
    git fetch origin && git checkout migration
    ```
  • Stow with the new host-aware recipe (auto-detects xps):
    ```bash
    just stow
    ```
  • Apply service state — mpd should already be enabled but this verifies:
    ```bash
    just services-enable
    systemctl --user is-enabled mpd.service mpd.socket
    ```
  • Pull xps-only mise tools:
    ```bash
    mise install
    ```
  • Cross-check: anything in mise ls that isn't in the post-split files should be either added to conf.d/xps.toml (forgotten) or actively removed via mise uninstall <tool>
  • Switch back to graphical session (Ctrl+Alt+F1 or restart niri)
  • Smoke test:
    • waybar shows XPS thermal zone (thermal-zone: 8, threshold 90)
    • BoxySVG / JetBrains Toolbox / WebApps / Tidewave appear in launcher (sourced from hosts/xps/)
    • tmux F12, ssh wrapper, claudecode, fff.nvim all work
    • mpd active and playing
  • just doctor — all green
  • If anything's broken: git checkout pc/xps && stow --dotfiles -S home idea git scripts terminal lazyvim vim systemd niri (and restore tarball if needed)

Phase 3 — Merge and clean up

After both machines have green Phase 1 and Phase 2.

  • Open PR migrationmain on GitHub; review the diff one more time
  • Squash-merge to main (cleaner history than fast-forward)
  • On daisy: git fetch && git checkout main && git pull (no working-tree changes — main now matches what was on migration)
  • On xps: same
  • Delete pc/xps from origin: gh api -X DELETE repos/rdlu/dotfiles/git/refs/heads/pc/xps
  • Delete local migration and remote migration on both machines: git branch -d migration && git push origin --delete migration
  • Update README.md to document the two-machine + per-host overlay model and the new just stow / just services-enable / just update / just doctor recipes

Risk points to keep in mind

  • The unstow→checkout→stow dance on xps (Phase 2) is the single highest-risk moment. If you skip the unstow on pc/xps first, the source files for mimeinfo.cache and *.target.wants/* symlinks disappear when you check out migration, and stow -D can't clean them up by name → dangling symlinks in ~/.config. Mitigation: the find ~/.config -xtype l check after unstow.
  • mpd silent disable on xps: removing the tracked *.target.wants/ symlinks without systemctl --user enable would silently disable mpd. just services-enable covers this — don't skip it. Verify with systemctl --user is-enabled mpd.service.
  • Mid-migration display flicker on xps: wpaperd/waybar/mako/swayidle get restarted as part of stow. Do Phase 2 from a TTY, not inside a niri terminal.
  • mise current state vs new files: mise install adds, doesn't remove. Run mise ls on each machine before splitting and cross-check after — anything installed but not in the post-split files was forgotten OR should be actively removed.
  • Sequential, not parallel: don't migrate both machines simultaneously. Daisy's first apply is the test of the new tooling.
  • Day delay between machines: if days pass between Phase 1 and Phase 2 and fixups land on migration in between, xps must git pull before its checkout step (the Phase 2 commands already include the fetch).
  • TPM clone on fresh-only: if ~/.tmux/plugins/tpm already exists on a machine, the (now gated) clone is a no-op. New machines get a fresh clone.
  • Pacman + mise overlap is intentional — see Justfile section. Don't try to dedupe.

Justfile improvements (folded in from review)

Per-host divergence already leaking into the justfile

  • Three near-identical stow recipes (stow, stow-daisy, stow-check) that disagree on package list. Collapse into a single stow HOST=$(hostname) recipe that stows base packages then stow hosts/<HOST>/ if present.
  • No restow / unstow / adopt recipes — first-install conflicts (real files where stow wants symlinks) have no escape hatch.

Distro coverage (cleanup)

  • README claims Fedora support but the justfile is Arch-only. Drop Fedora from README, delete setup/fedora-base.fish. WSL is also gone — delete setup/archlinux-wsl2.fish and the gnupg/ directory (which is just the WSL win-gpg-agent-relay.sh setup).

AUR helper standardization

  • packages recipe currently runs pacman -Sy paru. Keep it as a defensive no-op (already present on CachyOS); standardize every other recipe on paru -S (was yay -S). The user keeps yay for daily interactive use as a separate, optional install-yay recipe.

Pacman + mise overlap is intentional, not a bug

(Correcting an earlier note.) dev-setup installs nodejs npm python3 rustup zig erlang elixir via pacman, AND mise's config.toml pins versions for several of the same tools. This is deliberate two-tier:

  • Pacman = system-integrity floor (always distro-current, available at /usr/bin/... for anything system-level).
  • Mise = user-controlled project pin (mise activate shadows pacman in interactive shells).

Both coexist without conflict. The actionable item: ensure dev-setup ends with mise install so the user-pinned versions are bootstrapped on a fresh machine instead of waiting for first interactive use.

Idempotency / re-run safety

  • git clone .../tpm ~/.tmux/plugins/tpm fails on re-run (gate with test -d ... || …).
  • chsh -s /usr/bin/fish runs every time — check $SHELL first.
  • read -p is a bash-ism; recipes inherit sh/the user shell unless set shell := [\"bash\", \"-uc\"] is at the top of the justfile.
  • Manual edits in packages (reflector.conf, pacman.conf [chaotic-aur] block) could be made idempotent: grep -q '\\[chaotic-aur\\]' /etc/pacman.conf || sudo tee -a ....

Redundant work

  • stow --dotfiles -S terminal runs in fish-shell, kitty-terminal, and yazi-file-manager. Harmless but noisy. One stow: prereq, or stow at the end, would be cleaner.

Architecture-specific hard-coding

  • rustup toolchain add stable-x86_64-unknown-linux-gnu — fine for both current machines (both x86_64) but rustup default stable is more portable.

Missing day-2 recipes

  • update: re-stow, fisher update, ya pkg upgrade, nvim Lazy sync, mise upgrade, paru -Syu. Currently no single command brings an existing machine up to date.
  • doctor: check that fish/tmux/stow/mise are installed, that key symlinks exist, that the user shell is fish, that expected systemd user services are enabled, that mise tools resolve.
  • services-enable: consolidate systemctl --user enable calls into one recipe (replaces the tracked *.target.wants/ symlinks).

Small ergonomic wins

  • Use just's built-in [confirm] attribute on destructive recipes instead of read -p.
  • Add set shell := [\"bash\", \"-uc\"] and set dotenv-load at the top.

Mise per-host catalog (folded in)

Mise currently manages 30+ tools across cargo/pipx/ubi/native backends, with the config stowed via terminal/dot-config/mise/config.toml — both machines install the same set today. Goal: shared base + per-host overlays.

Mechanism: conf.d/ overlay

mise loads ~/.config/mise/config.toml plus everything in ~/.config/mise/conf.d/*.toml and merges the [tools] tables additively. Layout:

```
terminal/dot-config/mise/config.toml # shared base — cross-machine tools
hosts/xps/dot-config/mise/conf.d/xps.toml # xps-only tools
hosts/daisy/dot-config/mise/conf.d/daisy.toml # daisy-only tools
```

Stow handles the overlay the same way it would for waybar or .desktop files. Each machine ends up with a merged [tools] view; mise install pulls everything new.

Side benefit: conf.d/ also gives a clean place to drop temporary project tools without polluting the shared base.

Catalog audit (one-time, manual — happens during Phase 1/2)

The current [tools] table is one undifferentiated list. Splitting it requires deciding per-tool which file it belongs in. First-pass guesses (user to confirm):

  • Shared base: erlang, elixir, ruby, zig, gleam, python, pipx:llm, pipx:xxh-xxh, cargo:bottom, cargo:jaq, cargo:jnv, cargo:jj-cli, cargo:lazyjj, cargo:scooter, cargo:monolith, cargo:zellij, lazydocker, ubi:duckdb/duckdb, ubi:jesseduffield/lazydocker, pipx:epy-reader
  • Possibly host-specific: pipx:aider-chat, pipx:playwright, pipx:posting, pipx:pocket-tts, cargo:probe-rs-tools, cargo:rainfrog, cargo:gittype, cargo:ttyper, cargo:xleak

Why not deduplicate erlang/elixir/zig between pacman and mise? Because the dual install is intentional (see Justfile section).

Alternatives considered

  • MISE_PROFILE=$(hostname) — works, but needs an env var set everywhere and mise.<profile>.toml naming is less obvious than conf.d/.
  • One file with conditional templating — mise has no native hostname conditionals; would require a pre-stow render step. Worse than splitting files.
  • One mise file per machine, fully duplicated — what pc/xps would essentially be doing if mise diverged. Don't.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions