#+TITLE: Plan — Separate dotfiles from archsetup #+AUTHOR: Craig Jennings & Claude #+DATE: 2026-05-13 * Overview Extract =dotfiles/= from the =archsetup= repo into a standalone repository at =cjennings.net/dotfiles.git=. Archsetup keeps the install logic; dotfiles live in their own repo and get cloned-then-stowed at install time. Add a new =minimal/= stow target (Tier B — TUI-tooled headless server) for the =DESKTOP_ENV=none= path, alongside the existing =common/=, =dwm/=, and =hyprland/= targets. This is non-disturbing to a running session: all script edits, repo creates, and VM tests happen out-of-band. The only disruptive step is the final migration of existing machines (Phase 3), which unstows and re-stows local dotfile symlinks. * Decisions | # | Question | Answer | |----+-------------------------------------------+------------------------------| | Q1 | Extract dotfiles to a standalone repo? | Yes — now (not deferred) | | Q2 | Clone target when DOTFILES_REPO is set? | =~/.dotfiles= (convention) | | Q3 | What does DOTFILES_REPO unset mean? | No opt-out — always clones | | Q4 | Include a minimal/ headless target? | Yes — Tier B (TUI server) | | Q5 | Behavior when dotfiles dir isn't a git checkout? | Error out | ** Rationale notes - *Q1 = now.* Folds the extraction into this work rather than deferring it. Adds steps (create new repo, push content, update default URL) but produces a clean end-state in one pass. - *Q3 = no opt-out.* DOTFILES_REPO always has a default value (the new cjennings.net URL). The =DESKTOP_ENV=none= path stows =minimal/= rather than "no dotfiles at all". Simpler than supporting a SKIP_DOTFILES flag. - *Q4 = Tier B.* The headless-tooled server: shells + git + tmux + ssh + TUI apps (btop, htop, mc, ranger, tickrs, topgrade, wavemon) + the =.local/bin/= utility scripts. Craig runs TUI apps on personal servers regularly, so the "bare-bones" tier (Tier A) would feel sparse. - *Q5 = error out.* Strict. If the dotfiles dir somehow isn't a git checkout after archsetup's clone (manually modified, tarball extraction), archsetup refuses to proceed rather than silently skipping the =git restore= cleanup step. Prefers loud failure over surprising state. * Target tree — minimal/ The =minimal/= directory is a *standalone* stow target. It does NOT depend on =common/=; everything universal that =common/= ships is duplicated into =minimal/=. Drift between the two is a risk; a later refactor of =common/= to drop GUI bits (so =common/= itself becomes headless-safe) would eliminate the duplication, but that's out of scope here. #+begin_example minimal/ ├── .bash_logout ├── .bash_profile ├── .bashrc ├── .bashrc.d/ # 6 files: aliases, emacs, fzf, git, media, utilities ├── .config/ │ ├── btop/ # TUI system monitor │ ├── environment.d/ # PATH env (rofi/scripts entry stripped — GUI-only) │ │ └── envvars.conf │ ├── htop/ # TUI system monitor │ ├── mc/ # TUI file manager │ ├── ranger/ # TUI file manager │ ├── systemd/ │ │ └── user/ │ │ └── emacs.service # Emacs daemon — useful via ssh + emacsclient │ ├── tickrs/ # TUI stock ticker │ ├── topgrade.toml # CLI updater │ ├── user-dirs.dirs │ ├── user-dirs.locale │ └── wavemon/ # TUI WiFi monitor ├── .gitconfig # real config (templating later, in open-source cleanup) ├── .gitignore # user's global gitignore ├── .hushlogin ├── .local/ │ └── bin/ # full .local/bin/ from common/ — all CLI/TUI scripts │ # (notify, ifinstalled, et/em/ec, ssh-createkeys, │ # refresharchkeys, decryptfile/encryptfile, dab, │ # timezone-set, etc.) ├── .profile ├── .profile.d/ # 5 files: auto-tmux-session, browser, claude, display, framework ├── .ssh/ # 4 items: config, decrypt_ssh, set_perms, ssh.tar.gz.gpg ├── .stow-global-ignore ├── .stow-local-ignore ├── .tmux.conf ├── .zshrc └── .zshrc.d/ # 7 files: aliases, arch-downgrade, emacs, fzf, git, media, utilities #+end_example ** Excluded from minimal/ | Category | Reason | |-------------------------+-------------------------------------------------------| | Mail configs | =.mbsyncrc=, =.msmtprc=, =.authcode=, =.authinfo.gpg= — credential-bearing, separate open-source-release cleanup | | GTK / Qt theming | =.gtkrc-2.0=, =qt5ct/=, =qt6ct/=, =gtk-3.0/= — GUI-only | | X11 only | =.Xmodmap=, =.xscreensaver=, =sxhkd/=, =rofi/=, =feh/= | | GUI media tools | =audacious/=, =calibre/=, =mpv/=, =zathura/= — GUI-only; some credential-bearing | | Daemons w/ creds | =mpd/= (personal =~cjennings/= paths), =transmission/= (bcrypt RPC pass), =yt-dlp/= (email in config) — separate cleanup | | Notification / fonts | =dunst/=, =fontconfig/=, =mimeapps.list= — GUI-related | | Personal dirs | =documents/=, =music/=, =pictures/= — content, not config | | Display-only services | =systemd/user/geoclue-agent.service= — geolocation for gammastep red-shift; useless headless | | TUI music client | =ncmpcpp= — TUI but tied to mpd, which is excluded for credentials; SSH-into-desktop use case is too niche to ship | ** SSH and GPG availability Confirmed both are installed regardless of =DESKTOP_ENV=: - =openssh= is installed in =essential_services()= at =archsetup:1094= — always runs. - =gnupg= is installed in =desktop_environment()= at =archsetup:1649= — also always runs (function name is misleading; it doesn't skip when =DESKTOP_ENV=none=). The =desktop_environment()= function is a misnamed grab-bag — it installs fonts, qt themes, dunst, gnome-keyring (pure GUI) alongside universal authentication tools (gnupg, polkit, pass, rbw). On a =DESKTOP_ENV=none= install today, it ships 50+ pointless GUI packages just to get gnupg. A later refactor should split universal auth tools out into =essential_services= or its own step, but it's out of scope for this work. GPG key import remains a manual post-install step on every machine (unchanged from current behavior). The =gnupg= binary is installed; key import is the user's responsibility (=gpg --import keys.asc= from a backup or scp from another machine). The =.ssh/= directory in dotfiles uses a clever pattern: SSH keys are GPG-encrypted (=ssh.tar.gz.gpg=) and committed to the repo. After GPG keys are imported on a new machine, running =decrypt_ssh= recovers the real SSH keys. Shipping the encrypted archive in =minimal/= is safe. * Implementation plan Three phases. Each is a discrete unit with a stopping point. ** Phase 1 — Set up the new repo (~30 min, no archsetup changes) *** Step 1.1 — Create the bare repo on cjennings.net Manual SSH step (Craig). Probably: #+begin_src bash ssh git@cjennings.net "cd /var/git && git init --bare dotfiles.git" #+end_src Confirmed URL: =https://git.cjennings.net/dotfiles.git= (anonymous HTTPS read verified against existing repos at the same host on 2026-05-14). *** Step 1.2 — Extract dotfiles/ with filtered history In a temp working dir (does NOT touch the live archsetup repo): #+begin_src bash git clone --no-local /home/cjennings/code/archsetup /tmp/extract-dotfiles cd /tmp/extract-dotfiles git filter-repo --subdirectory-filter dotfiles/ #+end_src Result: a fresh repo where every commit is rewritten to be about files inside =dotfiles/= only. Per-file history and attribution preserved across the 275 commits. If =git-filter-repo= isn't installed, =pacman -S git-filter-repo= (it's in =extra=). *** Step 1.3 — Add minimal/ tree In =/tmp/extract-dotfiles/=: 1. Create =minimal/= directory. 2. Populate per the "Target tree" section above. Most files are direct copies from =common/= (=.bashrc=, =.profile=, etc.) plus the curated TUI app configs from =common/.config/=. 3. The =.local/bin/= is the full =common/.local/bin/= directory copied over verbatim — every CLI/TUI script. 4. Commit: =feat: add minimal/ stow target for headless installs=. *** Step 1.4 — Push to the new remote Push over SSH — anonymous HTTPS at =git.cjennings.net= is read-only, so the push remote uses the same SSH form as archsetup's own origin. The HTTPS URL stays the clone/read URL (Phase 2 config keys, Phase 3 clone). #+begin_src bash git remote add origin git@cjennings.net:dotfiles.git git push -u origin main #+end_src **Stops here.** =dotfiles.git= exists at cjennings.net with =common/=, =dwm/=, =hyprland/=, =minimal/=. Archsetup itself unchanged. No risk to running session or test infra. ** Phase 2 — Wire archsetup to the new repo (~1-2 hours + 40 min VM test) *** Step 2.1 — Add config keys to archsetup.conf.example Under the existing "Git Repositories" block (after the =dwm_repo= / =hyprland_repo= entries): #+begin_example # Dotfiles DOTFILES_REPO=https://git.cjennings.net/dotfiles.git DOTFILES_BRANCH=main DOTFILES_DIR=$HOME/.dotfiles #+end_example Document that the user's repo must contain =common/= plus =dwm/=, =hyprland/=, and/or =minimal/= subdirs that stow cleanly to =~=. *** Step 2.2 — Update archsetup script Edits to =/home/cjennings/code/archsetup/archsetup=: 1. *Read config* (around line 114-122): map =DOTFILES_REPO= / =DOTFILES_BRANCH= / =DOTFILES_DIR= env vars to lowercase script variables. 2. *Set defaults* (around line 136-146, alongside =dwm_repo= etc.): - =dotfiles_repo="${dotfiles_repo:-https://git.cjennings.net/dotfiles.git}"= - =dotfiles_branch="${dotfiles_branch:-main}"= - =dotfiles_dir="${dotfiles_dir:-/home/$username/.dotfiles}"= 3. *Validate* (in =validate_config()=, around line 155+): security-only check on =DOTFILES_REPO= (no leading dash, no whitespace/control chars) — same pattern as the existing =*_REPO= keys after =2c1377b=. 4. *Clone and stow* (in =user_customizations()=, currently around line 882-1010): - Replace the existing =dotfiles_dir="$user_archsetup_dir/dotfiles"= setup with =sudo -u "$username" git clone --depth 1 --branch "$dotfiles_branch" "$dotfiles_repo" "$dotfiles_dir"= (run as the target user; avoids a chown-after race if anything writes during the clone). - Per Q5: error out if =[ ! -d "$dotfiles_dir/.git" ]= after the clone. - Stow target based on =$desktop_env=: + =dwm= → stow =common/= + =dwm/= + =hyprland= → stow =common/= + =hyprland/= + =none= → stow =minimal/= only (NOT =common/=) - Guard the waybar-battery sed block (currently around line 856-865) and the =git restore= step (currently around line 896-902) so they only run on the dwm/hyprland paths. The minimal/ path skips both. *** Step 2.3 — Update test infra 1. =scripts/testing/archsetup-vm.conf=: add =DOTFILES_REPO=/tmp/dotfiles-test= (mirrors the =ARCHSETUP_REPO=/tmp/archsetup-test= pattern that's already there). 2. =scripts/testing/run-test.sh=: before VM launch, clone (or =cp -r=) the dotfiles repo to =/tmp/dotfiles-test= so the in-VM =git clone "$dotfiles_repo"= against the local path succeeds. *** Step 2.4 — VM test =make test= with default =DESKTOP_ENV=hyprland=. Verify: - Dotfiles clone from the new local path succeeds. - Stow pattern still works (=common/= + =hyprland/= go to =~/=). - No regressions from the install pre/post 2026-05-11 cleanup. Then re-run with =DESKTOP_ENV=none= to validate the new =minimal/= path. *Required*, not optional — =minimal/= ships untested otherwise. **Stops here.** =dotfiles/= is still in the archsetup repo. Local machines (this one, ratio, velox) are still using =~/.config/foo → ~/code/archsetup/dotfiles/foo= symlinks — *no migration done yet*. Running session safe. ** Phase 3 — Cleanup and migration (~15 min per machine, blocks on migration) *** Step 3.1 — Migrate this machine Order matters — clone first so the new tree is ready, then unstow and restow in immediate succession to minimize the gap when configs are missing: #+begin_src bash git clone https://git.cjennings.net/dotfiles.git ~/.dotfiles cd ~/code/archsetup make unstow hyprland # remove symlinks pointing at archsetup/dotfiles/ make stow hyprland DOTFILES=~/.dotfiles #+end_src *Don't run =hyprctl reload= or restart waybar/dunst between unstow and stow* — those reads would hit missing config files and could error or fall back to defaults. Hyprland keeps running with its in-memory config across the swap, so the gap is invisible as long as nothing reads the disk during it. After the stow finishes: =hyprctl reload=, restart waybar and dunst, verify nothing visibly broke. Repeat for ratio and velox at appropriate times. *** Step 3.2 — Remove dotfiles/ from archsetup repo Once all local machines are migrated: #+begin_src bash git rm -r dotfiles/ git commit -m "chore(archsetup): remove dotfiles/ — moved to dotfiles repo" #+end_src The directory is no longer needed since archsetup now clones from the external repo. *** Step 3.3 — Update CLAUDE.md Update the "Project Structure" and "Makefile Targets" sections to reflect the new layout. Document the =DOTFILES_REPO=, =DOTFILES_BRANCH=, and =DOTFILES_DIR= config keys. Note the =DOTFILES== Makefile override. Also document the post-install update flow so a future maintainer knows how to pull dotfile changes after archsetup runs: #+begin_src bash cd ~/.dotfiles && git pull cd ~/code/archsetup && make restow hyprland DOTFILES=~/.dotfiles #+end_src * Commit map Approximate commit boundaries for each phase. May split or combine as implementation reveals natural seams. ** Phase 1 commits (in the new dotfiles.git repo) | C | Subject | |-----+-------------------------------------------------------------| | D1 | initial import — filter-extracted from archsetup | | D2 | feat: add minimal/ stow target for headless installs | ** Phase 2 commits (in archsetup repo) | C | Subject | |-----+-------------------------------------------------------------| | A1 | feat(archsetup): add DOTFILES_REPO config keys + validation | | A2 | feat(archsetup): clone dotfiles repo, stow per DESKTOP_ENV | | A3 | refactor(testing): clone dotfiles repo in VM test setup | ** Phase 3 commits (in archsetup repo) | C | Subject | |-----+-------------------------------------------------------------| | A4 | chore(archsetup): remove dotfiles/ — moved to dotfiles repo | | A5 | docs: document external dotfiles layout in CLAUDE.md | * Open observations / future work These come up during the design but are out of scope for this work. - *=desktop_environment()= is a misnomer.* It installs both pure-GUI packages (fonts, qt themes, dunst, gnome-keyring) AND universal authentication tools (gnupg, polkit, pass, rbw). The latter belong in =essential_services()= or their own step. On =DESKTOP_ENV=none= installs today, you get 50+ pointless GUI packages just to get =gnupg=. Worth a follow-up refactor. - *Drift between common/ and minimal/.* Both ship =.bashrc=, =.profile=, =.gitconfig=, etc. If Craig updates one, the other rots. A future refactor could move all universal files out of =common/= and into a new shared base, with =common/= becoming "common GUI bits only". Out of scope here. Could be mitigated short-term by a CI check that errors if the duplicated files diverge. - *Personal-info cleanup in shipped configs.* =.gitconfig=, =.ssh/config=, =.config/yt-dlp/config=, =hyprland.conf:3=, etc. are all flagged in the open-source-release audit. This work ships =.gitconfig= and =.ssh/config= as-is (real values, not templated) — the templating belongs in the broader =[#A] Prepare for GitHub open-source release= cluster, where it can be done as a single consistent pass. - *Don't push the new dotfiles repo to GitHub until the secrets cleanup ships.* The repo at cjennings.net is fine for real values — it's Craig's private host. =minimal/.ssh/ssh.tar.gz.gpg= is GPG-encrypted (safe even in public), but =.gitconfig=, =.ssh/config=, and other configs in the tree carry personal info that shouldn't go on GitHub raw. - *Audit of dotfiles/common/ scripts.* The =[#B] Audit dotfiles/common directory= task in todo.org has unfinished subtasks for reviewing =.local/bin/= for unused scripts, orphaned configs, and unused stowed files. =minimal/= currently includes the full =.local/bin/= directory verbatim — a pruning pass on that audit would also improve =minimal/=. - *=common/.config/git/= and global gitignore.* The proposed =minimal/= ships =.gitconfig= and =.gitignore= at the top level. If Craig has anything under =.config/git/= worth shipping, add it. * Stopping points Pick a stopping point for this session: 1. *Phase 1 only* (~30 min). Get the new repo live, build =minimal/=, push. Stop. Pick up Phase 2 fresh later. *Recommended* — discrete unit, low risk, sets foundation. 2. *Phases 1 + 2* (~2-3 hours, includes a VM test). Repo live + archsetup wired + VM-tested. Stop before migrating local machines. Phase 3 happens whenever the migration is convenient. 3. *All three phases* (~3-5 hours). Full end-to-end. Long session. * Status (reviewed 2026-05-14) All open questions resolved. Pre-flight is done; the next session reads the spec body and executes Phase 1. | Item | Resolution | |----------------------------+-------------------------------------------------------------------------| | New repo URL | =https://git.cjennings.net/dotfiles.git= (anonymous HTTPS read verified)| | Bare repo path | =/var/git/dotfiles.git= (alongside existing =/var/git/*.git= repos) | | Scope for next session | Phase 1 only (~30 min) — repo + =minimal/= built and pushed | | =minimal/= tree | Spec tree + =environment.d/envvars.conf= (rofi path stripped) + =systemd/user/emacs.service= | | Phase 2 / Phase 3 | Constraints folded into the relevant sections; not executed today |