Plan the dots feature for preserving and syncing user configs
A whitelisted bare git repo over $HOME driven only by constrained omarchy dots commands: local snapshot history at batch boundaries plus squash-published state sync across machines. Design survived adversarial review; rejected alternatives and rationale recorded in the plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f4189398bc
commit
022f6993ba
+206
@@ -0,0 +1,206 @@
|
|||||||
|
# Plan: Dots — preserve and sync user configs out of the box
|
||||||
|
|
||||||
|
Revision 3. Rev 2 incorporated adversarial review by codex (xhigh) and grok;
|
||||||
|
rev 3 names the feature **dots** (`omarchy dots ...`) and adds the
|
||||||
|
multi-machine sync design.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Omarchy declares `~/.config` "your files" but does little to preserve them:
|
||||||
|
|
||||||
|
- `omarchy-refresh-config` litters `*.bak.<timestamp>` files next to originals.
|
||||||
|
- `omarchy-reinstall-configs` clobbers everything back to `/etc/skel` defaults.
|
||||||
|
- Snapper only snapshots the `root` config; recovering one config file from a
|
||||||
|
root snapshot is not a workflow, and `/home` may not be covered at all.
|
||||||
|
- The manual punts to a YouTube video about Stow.
|
||||||
|
|
||||||
|
There is no way to answer "what did the last update change about my configs?",
|
||||||
|
"restore my bindings from last week," or "make my new laptop feel like my
|
||||||
|
desktop."
|
||||||
|
|
||||||
|
## Rejected approaches
|
||||||
|
|
||||||
|
- **`git init ~/.config`**: dumping ground — Chromium profile, fcitx5 state,
|
||||||
|
app tokens, machine churn. A `.git` there gets discovered by editors and
|
||||||
|
prompts. Can't cover `~/.bashrc` or `~/.XCompose`.
|
||||||
|
- **`~/.config/omarchy` only**: too narrow; misses hypr, terminals, `.bashrc`.
|
||||||
|
- **Stow**: inverted model requiring file migration; organization, not history.
|
||||||
|
- **chezmoi / yadm**: third-party DSLs we'd be wrapping; overkill.
|
||||||
|
- **Raw git passthrough / lazygit over `$HOME`** (rejected in review):
|
||||||
|
`git clean -fd`, `reset --hard`, or "stage all" against a `$HOME` work tree
|
||||||
|
is a home-directory-eraser; `git add -A` can ingest `~/.ssh` and the object
|
||||||
|
store into itself (`status.showUntrackedFiles no` only affects `status`);
|
||||||
|
`remote add` + `push` silently defeats local-only. Experts can construct the
|
||||||
|
raw invocation themselves; Omarchy will not bless it.
|
||||||
|
- **Distributed git across machines**: two machines auto-committing timer and
|
||||||
|
update snapshots into a shared branch conflict constantly. History and sync
|
||||||
|
are different products (see Sync below).
|
||||||
|
|
||||||
|
## Chosen design: bare repo over `$HOME`, driven only by constrained commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init --bare ~/.local/share/omarchy/dots.git # mode 0700
|
||||||
|
```
|
||||||
|
|
||||||
|
No `.git` in any directory tools walk; files stay plain files in place. All
|
||||||
|
access goes through one internal helper that runs git **hermetically**:
|
||||||
|
|
||||||
|
- Repo-local config only: synthetic identity (`Omarchy <omarchy@localhost>`),
|
||||||
|
`commit.gpgsign=false`, `core.hooksPath` disabled, `--no-verify`,
|
||||||
|
`GIT_CONFIG_GLOBAL=/dev/null` so user signing/hooks/templates/excludes and a
|
||||||
|
`$HOME/.gitignore` can never break or intercept an automatic commit.
|
||||||
|
- Never export `GIT_DIR`/`GIT_WORK_TREE` (would leak into `omarchy-plugin-*`,
|
||||||
|
`omarchy-theme-update`, `omarchy-update-dev`, user hooks).
|
||||||
|
- Serialized via a lock; concurrent refresh/update/manual snapshots queue.
|
||||||
|
- **Best-effort everywhere**: a failed snapshot warns and continues. History
|
||||||
|
is optional; updates are not. Never on the failure path of
|
||||||
|
`omarchy-update`/`omarchy-migrate` (`set -e` there must not see git errors).
|
||||||
|
|
||||||
|
### Audited manifest, not a derived whitelist
|
||||||
|
|
||||||
|
Tracking is `git add -f --pathspec-from-file=<manifest>` only. The manifest is
|
||||||
|
a hand-audited file shipped with Omarchy — explicitly **not** derived from
|
||||||
|
`$OMARCHY_PATH/config` (which ships Chromium Preferences, fcitx5,
|
||||||
|
`opencode/opencode.json` where users put API keys, etc.):
|
||||||
|
|
||||||
|
- Include: `.config/hypr/*.{lua,conf}`, `.config/omarchy/shell.json`,
|
||||||
|
`.config/omarchy/extensions/**`, `.config/omarchy/hooks/**`,
|
||||||
|
terminal configs (alacritty/foot/ghostty/kitty), `.config/btop/btop.conf`,
|
||||||
|
`.config/starship.toml`, `.bashrc`, `.XCompose`.
|
||||||
|
- Exclude (deliberately): `.config/omarchy/plugins/**` and
|
||||||
|
`.config/omarchy/themes/**` (nested git clones managed by
|
||||||
|
`omarchy-plugin-*` / `omarchy-theme-update`), backgrounds (multi-MB
|
||||||
|
binaries), `.config/btop/themes/` (symlink into `~/.local/state`), anything
|
||||||
|
Chromium/fcitx5/opencode/xournalpp.
|
||||||
|
- **Two tiers**: every manifest path is history-tracked, but some are marked
|
||||||
|
`local` — machine-specific files that sync must never touch. At minimum
|
||||||
|
`monitors.lua`; likely hardware quirks like `dell-haptic.conf`. This is the
|
||||||
|
lightweight answer to chezmoi's hostname templates: exclude, no DSL.
|
||||||
|
- Once committed, a secret lives in the object store forever; "no remote" is
|
||||||
|
not a secrets story. The manifest is the deny line — nothing outside it is
|
||||||
|
ever staged, and there is no command that stages more.
|
||||||
|
|
||||||
|
### Dotfile-manager detection: stand down, don't fight
|
||||||
|
|
||||||
|
`manual/31-dotfiles.md` sends users to Stow today, and migrations deliberately
|
||||||
|
write *through* symlinks. For those users git records an unchanged symlink, so
|
||||||
|
snapshots would be silent no-ops — and `restore` could stomp a symlink with a
|
||||||
|
regular file. Therefore:
|
||||||
|
|
||||||
|
- At seed time and before each snapshot: if manifest paths are symlinks or a
|
||||||
|
known manager (yadm, chezmoi, existing bare-repo alias, `~/.git`) is
|
||||||
|
detected, mark the repo dormant and say so in `omarchy dots status`.
|
||||||
|
- `restore` refuses type changes (symlink↔file) and paths outside the
|
||||||
|
manifest.
|
||||||
|
- These users keep `.bak` behavior, which genuinely works for them (copies
|
||||||
|
content, not links).
|
||||||
|
|
||||||
|
### Snapshot points: batch boundaries, before *and* after
|
||||||
|
|
||||||
|
The unit is a labeled snapshot pair around each *batch* mutation, via an
|
||||||
|
internal `omarchy-dots-snapshot "<label>"`:
|
||||||
|
|
||||||
|
1. **`omarchy-provision-user`**: seed repo + initial commit, after `git.sh`
|
||||||
|
and `.XCompose` are in place. Idempotent; `--force` must not re-init.
|
||||||
|
2. **`omarchy-migrate`**: before/after pair labeled with version. Covers both
|
||||||
|
`omarchy-update` *and* the login-time migration path, and separates
|
||||||
|
migration changes from prior user dirt — the "before" commit captures user
|
||||||
|
edits, the "after" commit is purely what migrations did.
|
||||||
|
3. **Batch refresh commands** (`omarchy-refresh-hyprland` etc.): one pair per
|
||||||
|
command, not one commit per `omarchy-refresh-config` call (hyprland calls
|
||||||
|
it seven times).
|
||||||
|
4. **`omarchy-reinstall-configs`**: a "before" snapshot — honestly labeled.
|
||||||
|
This operation replays all of `/etc/skel`; a manifest repo does **not**
|
||||||
|
make it reversible and we don't claim it does.
|
||||||
|
5. **Timer**: an hourly-ish systemd user timer committing only if dirty, so
|
||||||
|
hand edits are captured — without it, "restore my bindings from last week"
|
||||||
|
fails for any edit not followed by an Omarchy operation.
|
||||||
|
|
||||||
|
`.bak` files **stay** in v1. They serve symlink users, they're documented
|
||||||
|
(manual, agent skill, `refresh-config-test.sh` asserts them), and git history
|
||||||
|
is invisible until the menu surface exists. Removal is a later, separate
|
||||||
|
decision once restore is on *Update > Config* and proven.
|
||||||
|
|
||||||
|
## Sync across machines: publish/pull of state, not history
|
||||||
|
|
||||||
|
The local snapshot history is machine-generated noise (timer commits,
|
||||||
|
before/after pairs) and stays private. Sync happens on a shared `sync` branch
|
||||||
|
in the same repo that only two operations touch:
|
||||||
|
|
||||||
|
- **`omarchy dots push`** — *squash-publishes* the current manifest state
|
||||||
|
(minus `local`-tier files) as one commit: "Published from <hostname>". Only
|
||||||
|
present-state files leave the machine — a token the timer captured last
|
||||||
|
month and you since deleted is not in any published commit.
|
||||||
|
- **`omarchy dots pull [url]`** — fetches `sync`, takes a local pre-pull
|
||||||
|
snapshot (so pull is undoable via `restore`), then applies the files.
|
||||||
|
Never touches `local`-tier files. With a URL argument on a fresh machine,
|
||||||
|
it configures the remote first — the second-machine straight shot is one
|
||||||
|
command.
|
||||||
|
|
||||||
|
Conflict story stays simple because publishes are whole-state: push refuses
|
||||||
|
when the remote moved since your last pull ("pull first"); pull attempts a
|
||||||
|
per-file three-way merge and falls back to taking remote — your version is
|
||||||
|
one `omarchy dots restore` away.
|
||||||
|
|
||||||
|
Remote setup UX:
|
||||||
|
|
||||||
|
- `omarchy dots push` with no remote runs setup inline: paste a git URL, or
|
||||||
|
create a **private** GitHub repo via `gh` when authenticated. Verify the
|
||||||
|
repo is private; warn loudly if not.
|
||||||
|
- Manual by default. Push is an intentional "my setup is good" moment — no
|
||||||
|
auto-push ever (publishing half-finished experiments is an anti-feature).
|
||||||
|
Opt-in *pull on login* later for people with one canonical machine; safe
|
||||||
|
because every pull is snapshot-guarded.
|
||||||
|
- Dormant mode (dotfile-manager users) disables sync too — they have their
|
||||||
|
own.
|
||||||
|
- v2 candidate: `omarchy-provision-first-run` asks "Got a dots repo?" so a
|
||||||
|
new machine is yours before first login completes. Held back from v1 —
|
||||||
|
drags SSH/auth bootstrapping into first-run.
|
||||||
|
|
||||||
|
## User-facing surface (v1)
|
||||||
|
|
||||||
|
Constrained commands only — a new `dots` group in `GROUP_DESCRIPTIONS`,
|
||||||
|
invoked as `omarchy dots <verb>`:
|
||||||
|
|
||||||
|
- `omarchy dots snapshot [label]` — take a snapshot now
|
||||||
|
- `omarchy dots log` — history with labels
|
||||||
|
- `omarchy dots diff [ref]` — default range: the last before/after pair
|
||||||
|
- `omarchy dots restore <file> [--at <ref>]` — accepts `~/.config/...`,
|
||||||
|
`.config/...`, or manifest-relative paths; takes a pre-restore snapshot
|
||||||
|
first so restore is itself undoable; refuses untracked paths and type
|
||||||
|
changes; confirms over dirty files
|
||||||
|
- `omarchy dots push` / `omarchy dots pull [url]` — sync (above)
|
||||||
|
- `omarchy dots status` — repo state, dormant/active, remote, last
|
||||||
|
snapshot/publish
|
||||||
|
|
||||||
|
No raw git surface. No lazygit integration.
|
||||||
|
|
||||||
|
## Rollout
|
||||||
|
|
||||||
|
- New users: seeded by `omarchy-provision-user`.
|
||||||
|
- Existing users: migration inits the repo + initial commit — **skippable and
|
||||||
|
bounded**: no-op if repo path exists, another manager is detected, or
|
||||||
|
identity can't be synthesized; never adds beyond the manifest; never fails
|
||||||
|
`omarchy update`.
|
||||||
|
- Docs/tests alignment: manual `31-dotfiles.md` gets the built-in story
|
||||||
|
(honest framing: local event history + sync of published states, *not* a
|
||||||
|
backup — the local repo dies with the disk); a `docs/dots.md` reference doc
|
||||||
|
per the documentation layout; `default/agents/skills/omarchy/SKILL.md`
|
||||||
|
updated; CLI routing/metadata tests extended; new tests for identity-less
|
||||||
|
commit, batch snapshot pairing, restore round-trip, dormant-mode detection,
|
||||||
|
push/pull round-trip against a local bare remote, `local`-tier exclusion,
|
||||||
|
and "git failure must not fail update".
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
1. Manifest location: `default/omarchy/dots-manifest` vs alongside the
|
||||||
|
helper; and whether users may extend it via
|
||||||
|
`~/.config/omarchy/dots-manifest.d/`.
|
||||||
|
2. Whether `.config/omarchy/hooks/**` belongs in the manifest at all (small
|
||||||
|
scripts, but the likeliest place for a pasted token).
|
||||||
|
3. Harden `omarchy-refresh-config` against `..` path escape (documented today
|
||||||
|
in AGENTS.md) as part of this work or separately.
|
||||||
|
4. Timer cadence (hourly vs daily) and whether the dirty-check should debounce
|
||||||
|
against an active editing session.
|
||||||
|
5. Whether `input.lua` is `local`-tier (keyboard layouts travel, trackpad
|
||||||
|
quirks don't).
|
||||||
Reference in New Issue
Block a user