aboutsummaryrefslogtreecommitdiff
path: root/docs/PLAN-dotfiles-separation.org
blob: 8ef583f75f557348495d96532943b8572582e3b1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
#+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       |