From 1fd8e7e77375c34ad9dc9d1126002f72594b9c54 Mon Sep 17 00:00:00 2001 From: alexion Date: Mon, 6 Jul 2026 16:19:06 -0400 Subject: [PATCH] dotcli: add dot setup folders (XDG short-name migration) Adds a dot setup subcommand family (idempotent, re-runnable machine-setup tasks) with a folders task that brings the 8 standard XDG user directories under a fixed short-name convention (Desktop -> .desktop, Documents -> doc, Downloads -> dwn, Music -> mus, Pictures -> pic, Videos -> vid, Templates/Public -> .ignoreme), hardcoded rather than read from ~/.config/user-dirs.dirs -- that file is a separate, manually tracked dotfile whose values can drift or go stale, and the migration must not depend on it being correct. The nested Pictures/Screenshots folder is renamed to pic/screenshots in the same pass. Content left behind in a legacy full-named folder by a fresh XDG-defaults install -- empty or not -- is merged into its short-named replacement. A same-named collision between a legacy folder and its target is never overwritten: it is skipped, reported, and the legacy folder is left in place rather than deleted while something in it could not be merged. --dry-run previews what would move/skip without touching the filesystem. xdg-user-dirs-update runs once afterward to notify running apps/portals. --- .../0006-setup-dispatcher-and-folders-core.md | 44 ++- .../0007-folders-non-empty-confirmation.md | 30 +- .../tasks/0008-folders-collision-handling.md | 33 +- .../tasks/000b-folders-unconditional-merge.md | 79 ++++ .../000c-folders-fixed-target-mapping.md | 64 ++++ .config/dot/commands/setup/folders.fish | 134 +++++++ .config/dot/commands/setup/setup.fish | 35 ++ .config/dot/tests/dot.fish | 357 ++++++++++++++++++ .config/fish/completions/dot.fish | 5 + .config/user-dirs.dirs | 16 + .github/README.md | 2 + 11 files changed, 783 insertions(+), 16 deletions(-) create mode 100644 .config/dot/.claude/tasks/000b-folders-unconditional-merge.md create mode 100644 .config/dot/.claude/tasks/000c-folders-fixed-target-mapping.md create mode 100644 .config/dot/commands/setup/folders.fish create mode 100644 .config/dot/commands/setup/setup.fish create mode 100644 .config/user-dirs.dirs diff --git a/.config/dot/.claude/tasks/0006-setup-dispatcher-and-folders-core.md b/.config/dot/.claude/tasks/0006-setup-dispatcher-and-folders-core.md index 4d83147..6b638c1 100644 --- a/.config/dot/.claude/tasks/0006-setup-dispatcher-and-folders-core.md +++ b/.config/dot/.claude/tasks/0006-setup-dispatcher-and-folders-core.md @@ -41,20 +41,50 @@ duplication point, and a README command-table row. ## Acceptance criteria -- [ ] `dot setup folders` on a fresh scratch `$HOME` (all 8 legacy folders +- [x] `dot setup folders` on a fresh scratch `$HOME` (all 8 legacy folders present and empty) renames them to their short-name targets per the mapping table, including `Pictures/Screenshots→pic/screenshots`, and leaves the tracked `user-dirs.dirs` short names in place -- [ ] The fake `xdg-user-dirs-update` (PATH-prepended, logging its invocation +- [x] The fake `xdg-user-dirs-update` (PATH-prepended, logging its invocation per the project's existing fake-`sudo`/fake-`pacman` testing pattern) is invoked exactly once after a successful migration -- [ ] Bare `dot setup` on a fresh scratch `$HOME` runs the `folders` task as +- [x] Bare `dot setup` on a fresh scratch `$HOME` runs the `folders` task as part of running everything -- [ ] `dot setup folders help` and `dot setup help` print usage and make no +- [x] `dot setup folders help` and `dot setup help` print usage and make no filesystem changes -- [ ] Re-running `dot setup folders` after a clean migration is a no-op +- [x] Re-running `dot setup folders` after a clean migration is a no-op (idempotent) -- [ ] `~/.github/README.md` has a command-table row for `dot setup` +- [x] `~/.github/README.md` has a command-table row for `dot setup` (and its `folders` task) with paths relative to `$HOME` -- [ ] `~/.config/dot/tests/dot.fish` covers the above cases and +- [x] `~/.config/dot/tests/dot.fish` covers the above cases and `fishtape ~/.config/dot/tests/dot.fish` passes + +## Implementation Notes + +- The desired short names for `dot setup folders` are read directly from the + tracked `~/.config/user-dirs.dirs` (parsed via `grep`/`string match`, not + sourced as shell), per the parent spec's decision that this file is the + single source of truth. This machine's real `user-dirs.dirs` was + deliberately left untouched/untracked and no live migration was run against + this machine's actual home directory — the user chose "code + tests only" + scope for this task (a real rename of `~/Desktop`, `~/Documents`, etc. is a + separate, explicit action to take later), so only the scratch-`$HOME` + fishtape fixtures exercise the short-name `user-dirs.dirs` content. + Tracking the real file and running the real migration remains open. +- During `/review-uncommitted`, the spec-fidelity pass caught a real bug: the + nested `Pictures/Screenshots→pic/screenshots` move ran unconditionally, + before checking whether `Pictures` held other, unrelated content — so a + `Pictures` folder with both `Screenshots/` and some other file got + partially mutated (Screenshots pulled out) while still being reported as + "left in place." Fixed by gating the Screenshots move on the rest of the + folder being empty too; added a regression test for this case + ("Screenshots is not peeled off... when Pictures still has other + content"). +- Completions (`~/.config/fish/completions/dot.fish`) got a `dot setup` + block mirroring `dot kde`'s per-subcommand completion entries, even though + the task's required "completions/help-glob duplication point" is already + satisfied automatically by the existing generic directory glob (no changes + were needed there for `dot setup`/`dot help` to discover the new nested + command). The added completions are a small polish addition beyond the + strict letter of the acceptance criteria, consistent with the existing + `kde` subcommand's treatment. diff --git a/.config/dot/.claude/tasks/0007-folders-non-empty-confirmation.md b/.config/dot/.claude/tasks/0007-folders-non-empty-confirmation.md index 4ccd369..76f1b6f 100644 --- a/.config/dot/.claude/tasks/0007-folders-non-empty-confirmation.md +++ b/.config/dot/.claude/tasks/0007-folders-non-empty-confirmation.md @@ -19,12 +19,34 @@ rule. ## Acceptance criteria -- [ ] A legacy folder with real content (a real file, not just an empty +- [x] A legacy folder with real content (a real file, not just an empty directory) refuses to migrate without `--yes`, prints what would have been moved, and leaves the folder and its contents untouched -- [ ] The same legacy folder migrates successfully when `--yes` is passed -- [ ] A legacy folder containing only a stray dotfile/metadata file (e.g. a +- [x] The same legacy folder migrates successfully when `--yes` is passed +- [x] A legacy folder containing only a stray dotfile/metadata file (e.g. a fake `.directory`) is still treated as non-empty and triggers the same confirmation gate -- [ ] `~/.config/dot/tests/dot.fish` covers the above cases and +- [x] `~/.config/dot/tests/dot.fish` covers the above cases and `fishtape ~/.config/dot/tests/dot.fish` passes + +## Implementation Notes + +- `--yes`'s actual move reuses the exact same branch shape as the existing + silent-empty path (rename `Screenshots` → `screenshots` when present, then + `rmdir` the legacy folder), extended to also `mv` any remaining top-level + entries into the target first. Screenshots is always moved as one atomic + unit — its individual files are never mv'd/reported separately — so a + non-empty `Screenshots` (own acceptance criterion in the parent spec) is + gated and migrated the same way a non-empty top-level file would be. +- Collision handling (no-clobber `mv -n`, reporting skipped files, leaving the + legacy folder in place on a collision) is explicitly out of scope here — + it's owned by 0008-folders-collision-handling.md, per that task's own + frontmatter/spec section. The `--yes` path added here uses a plain `mv`. +- `/review-uncommitted` flagged two minor issues, both fixed: a stale comment + claiming a helper variable was used by both the silent-empty and `--yes` + paths when it was only read by the latter, and a duplicated `find` + invocation computing the same top-level listing twice under one condition + (now computed once and reused). It also flagged the non-empty "would move" + preview listing recursively-nested files individually instead of treating + `Screenshots` as one unit like the real move does — fixed so the preview + and the actual move share the same top-level-entries list. diff --git a/.config/dot/.claude/tasks/0008-folders-collision-handling.md b/.config/dot/.claude/tasks/0008-folders-collision-handling.md index 16bf3aa..9e29b16 100644 --- a/.config/dot/.claude/tasks/0008-folders-collision-handling.md +++ b/.config/dot/.claude/tasks/0008-folders-collision-handling.md @@ -18,15 +18,38 @@ $to`, which had no collision protection at all. ## Acceptance criteria -- [ ] A filename collision between a legacy folder and its already-populated +- [x] A filename collision between a legacy folder and its already-populated short-named target is skipped, not overwritten (the target's existing file is preserved byte-for-byte) -- [ ] The skipped collision is reported to the user -- [ ] The legacy folder is left in place (not removed) when a collision +- [x] The skipped collision is reported to the user +- [x] The legacy folder is left in place (not removed) when a collision occurred, even though `--yes` was given and other non-colliding files in it were moved -- [ ] Re-running `dot setup folders` after a collision was reported and left +- [x] Re-running `dot setup folders` after a collision was reported and left in place behaves consistently (doesn't lose the previously-skipped file, doesn't re-move already-migrated files) -- [ ] `~/.config/dot/tests/dot.fish` covers the above cases and +- [x] `~/.config/dot/tests/dot.fish` covers the above cases and `fishtape ~/.config/dot/tests/dot.fish` passes + +## Implementation Notes + +- The two prior branches (silent-empty merge vs. `--yes`-confirmed merge) + were unified into one `if test (count $other_entries) -eq 0; or set -q + _flag_yes` branch, since the collision-detection/no-clobber logic is + identical either way. This has one side effect beyond the letter of the + acceptance criteria (which frame collision handling around the `--yes` + path): a legacy folder that's otherwise "empty" except for an emptyish + nested `Screenshots` dir now also gets collision-checked against an + already-populated `pic/screenshots` on the silent, no-`--yes` path. This + closes the same unguarded-`mv` gap the spec calls out as the motivating + problem (the old code's silent-path `mv $screenshots_path + $target_path/screenshots` had no collision protection at all either), so + it was kept rather than special-cased away. Covered by its own test + ("a silent-path Screenshots collision ..."). +- Collision detection is a pre-check (`test -e $target_path/...`) before an + actual `mv -n`, rather than relying on `mv -n`'s exit code alone, so each + colliding entry can be individually identified and reported by path. +- `/review-uncommitted` (risk: Medium, standards: 0 hard violations, spec: + 0 missing/wrong requirements) raised no changes needed; the one scope note + it flagged (the silent-path Screenshots case above) was a deliberate, + judged-correct decision rather than an oversight. diff --git a/.config/dot/.claude/tasks/000b-folders-unconditional-merge.md b/.config/dot/.claude/tasks/000b-folders-unconditional-merge.md new file mode 100644 index 0000000..9084ff3 --- /dev/null +++ b/.config/dot/.claude/tasks/000b-folders-unconditional-merge.md @@ -0,0 +1,79 @@ +--- +spec: dot-setup-folders +blocked-by: 0008-folders-collision-handling +--- + +## What to build + +Remove the `--yes` confirmation gate that 0007/0008 built: a legacy folder +with real content in it is migrated unconditionally now, the same as an +empty one, since the collision handling from 0008 already makes the merge +non-destructive on its own (a same-named entry is never overwritten, and the +legacy folder is kept whenever any collision occurred). The `--yes` gate +turned out to protect against a scenario collision handling already +prevents, while making the everyday case — a machine that already has real +files in `~/Documents`, `~/Pictures`, etc. — a silent no-op unless the flag +was remembered, which defeats the point of the task. + +In its place: + +- `dot setup folders` always attempts the merge for every legacy folder, + content or none. +- A new `--dry-run` flag replaces `--yes` in the flag slot: it reports what + would move and what would be skipped as a collision, without touching the + filesystem at all (no `mkdir`, no `mv`/`rmdir`, no `xdg-user-dirs-update`). +- A real (non-dry-run) run now reports what it moved per legacy folder + (e.g. `moved 12 entries from ~/Documents to ~/doc`), instead of staying + silent on success. A folder where nothing top-level moved (already empty, + or everything in it collided) prints no such line — only non-trivial moves + and collisions produce output. +- `--yes` is removed outright (not kept as a silent no-op): passing it now + fails with argparse's standard unknown-option error. + +## Acceptance criteria + +- [x] A legacy folder with real content merges on a plain `dot setup + folders`, with no flag required +- [x] A real run prints `moved N entries from ~/ to ~/` for a + folder where top-level entries actually moved, and nothing for a + folder where none did +- [x] A real run prints a dedicated line when the nested Screenshots folder + itself is moved (e.g. `moved ~/Pictures/Screenshots to ~/pic/screenshots`) +- [x] Collision detection/reporting and the "leave the legacy folder in + place when a collision occurred" behavior from 0008 are unchanged + under the new unconditional default +- [x] `--dry-run` reports the same would-move/would-skip information without + creating any target directory, moving/removing anything, or invoking + `xdg-user-dirs-update` +- [x] `dot setup folders --yes` fails with an unknown-option error (argparse + default), rather than being silently accepted or gated on +- [x] `dot setup folders help` output no longer mentions `--yes` and + documents `--dry-run` instead +- [x] Idempotency holds: re-running after a clean merge, and re-running + after a collision was reported, both behave the same as before +- [x] `~/.config/dot/tests/dot.fish` is updated to exercise the above + (replacing the old `--yes`-gated cases) and + `fishtape ~/.config/dot/tests/dot.fish` passes + +## Implementation Notes + +- The `--yes` gate and the `screenshots_emptyish`/`other_entries` machinery + that computed it were deleted outright rather than special-cased away: + once merging is unconditional, that machinery had no remaining purpose + (it existed solely to decide "empty enough to skip the gate"). +- `mkdir -p $target_path` and the final `xdg-user-dirs-update` are both now + guarded by `not set -q _flag_dry_run`, making `--dry-run` a true no-op + rather than "no-op except for directory scaffolding." +- Collision detection (`test -e $target_path/...`) runs identically in both + modes; `--dry-run` only gates the actual `mv`/`rmdir`/`mkdir` calls, so the + reported would-move/would-skip split is exactly what a real run would do. +- Success reporting is per-legacy-folder and suppressed at zero: a folder + that was already empty (or whose only entries all collided) prints + nothing, so a routine re-run stays quiet like before. +- All prior collision/idempotency/Screenshots test scenarios were kept, + just re-pointed at the plain `dot setup folders` invocation instead of + `--yes`; two scenarios that only differed by which code branch (`--yes` + vs. silent-empty) they exercised now hit the same branch, but were both + kept since they still cover distinct fixture shapes (Pictures with vs. + without unrelated top-level content alongside a colliding Screenshots). +- `fishtape ~/.config/dot/tests/dot.fish` passes (178 tests). diff --git a/.config/dot/.claude/tasks/000c-folders-fixed-target-mapping.md b/.config/dot/.claude/tasks/000c-folders-fixed-target-mapping.md new file mode 100644 index 0000000..9457b08 --- /dev/null +++ b/.config/dot/.claude/tasks/000c-folders-fixed-target-mapping.md @@ -0,0 +1,64 @@ +--- +spec: dot-setup-folders +blocked-by: 000b-folders-unconditional-merge +--- + +## What to build + +Stop reading the short-name target from `~/.config/user-dirs.dirs` and +hardcode the legacy-name -> short-name mapping directly in +`_dot_setup_folders`, dropping the dependency on that file entirely. + +The original design treated the tracked `user-dirs.dirs` as the single +source of truth for target names, assuming someone would hand-edit it to +the short names before ever running the command. On this machine that +never happened: the tracked file still had the stock XDG defaults +(`XDG_DOCUMENTS_DIR="$HOME/Documents"`, etc.), so `target_path` resolved to +the exact same directory as `legacy_path` for every folder. The migration +logic then reported every entry as a "collision" against itself instead of +moving anything -- a confusing, silent-feeling failure rather than an +actual migration. + +The short names are fixed (`.desktop`, `doc`, `dwn`, `mus`, `pic`, `vid`, +`.ignoreme`) and not meant to be configurable, so there's nothing to read +from a file in the first place. `user-dirs.dirs` remains a separate, +manually tracked dotfile (edited and tracked by hand, like any other +dotfile) for apps/`xdg-user-dirs-update` to consult -- `dot setup folders` +itself no longer reads it, requires its presence, or writes to it. + +## Acceptance criteria + +- [x] `_dot_setup_folders` no longer reads, parses, or requires + `~/.config/user-dirs.dirs`; the legacy->short-name mapping is a fixed + table in the function itself +- [x] Migration works identically whether `user-dirs.dirs` is absent, + empty, or declares stale/full-name values (the exact real-world case) +- [x] `user-dirs.dirs` is left byte-for-byte untouched by `dot setup + folders` when present, and no file is created when absent +- [x] `dot setup folders help` no longer describes reading target names + from `user-dirs.dirs` +- [x] `~/.config/dot/tests/dot.fish` no longer seeds a `user-dirs.dirs` + fixture as a migration precondition, and covers the stale/missing + cases above; `fishtape ~/.config/dot/tests/dot.fish` passes + +## Implementation Notes + +- Replaced the `xdg_vars`/`grep`/`string match` parsing of `user-dirs.dirs` + with two parallel hardcoded arrays, `legacy_names` and `target_names`, + indexed together -- same shape the code already used for `legacy_names` + alone, just extended to cover the target side too. +- The early `if not test -f $user_dirs; return 1` guard was deleted outright + rather than kept as a soft check: there's nothing left for the function to + read from that file, so requiring its existence would just be a + vestigial, unjustifiable precondition. +- Removed the `short_name_user_dirs` fixture and its seeding step from every + test scenario (it was previously duplicated into ~15 scenarios as a + migration precondition); added two new scenarios instead: one reproducing + the exact real-machine bug (stale full-name `user-dirs.dirs` values) and + one confirming migration works with no `user-dirs.dirs` file at all. +- Verified against this machine's real, still-stale `~/.config/user-dirs.dirs` + via `_dot_setup_folders --dry-run`: previously reported every entry in + Desktop/Documents/Downloads/Pictures/Videos as a collision against + itself; now correctly reports `would move N entries from ~/Documents to + ~/doc` etc. +- `fishtape ~/.config/dot/tests/dot.fish` passes (183 tests). diff --git a/.config/dot/commands/setup/folders.fish b/.config/dot/commands/setup/folders.fish new file mode 100644 index 0000000..be70962 --- /dev/null +++ b/.config/dot/commands/setup/folders.fish @@ -0,0 +1,134 @@ +function _dot_setup_folders_usage + echo "usage: dot setup folders [--dry-run] + +Brings the 8 standard XDG user directories under the project's fixed +short-name convention (Desktop -> .desktop, Documents -> doc, Downloads -> +dwn, Music -> mus, Pictures -> pic, Videos -> vid, Templates/Public -> +.ignoreme). This mapping is fixed and does not depend on +~/.config/user-dirs.dirs, which is a separate, manually tracked dotfile +this command never reads or writes. + +Content left behind in a legacy full-named folder (e.g. ~/Documents) by a +fresh XDG-defaults install -- empty or not -- is merged into its short-named +replacement. A nested Pictures/Screenshots folder is renamed to +pic/screenshots as part of the same pass. + +An entry that collides by name with something already in the short-named +target is never overwritten: it's skipped, reported, and its legacy folder is +left in place (not removed) even when everything else in it migrated. + + --dry-run report what would move and what would be skipped as a + collision, without changing anything on disk + +Runs xdg-user-dirs-update once afterwards to notify running apps/portals +(skipped under --dry-run)." +end + +function _dot_setup_folders + if test "$argv[1]" = help + _dot_setup_folders_usage + return 0 + end + + argparse 'dry-run' -- $argv + or return 1 + + # Fixed legacy-name -> short-name mapping. Deliberately hardcoded rather + # than read from ~/.config/user-dirs.dirs: that file is a separate, + # manually tracked dotfile whose XDG_*_DIR values can drift or go stale + # (or never get edited to the short names at all), and this command's + # own migration logic must not depend on it being correct. + set -l legacy_names Desktop Documents Downloads Music Pictures Videos Templates Public + set -l target_names .desktop doc dwn mus pic vid .ignoreme .ignoreme + + for i in (seq (count $legacy_names)) + set -l legacy_name $legacy_names[$i] + set -l target_rel $target_names[$i] + set -l target_path $HOME/$target_rel + set -l legacy_path $HOME/$legacy_name + + if not set -q _flag_dry_run + mkdir -p $target_path + end + + if not test -d $legacy_path + continue + end + + # Screenshots is always moved as one atomic unit (renamed to + # lowercase screenshots), so its individual files must never appear + # as separate move/report entries. + set -l screenshots_path $legacy_path/Screenshots + set -l top_level_entries (find $legacy_path -mindepth 1 -maxdepth 1 -not -name Screenshots) + + # No-clobber: an entry whose name already exists in the target is + # never moved over. It's collected here and reported below; its + # legacy folder is left in place (not removed) if any collision + # occurred, even though everything else in it migrated successfully. + set -l collisions + set -l movable_entries + set -l screenshots_movable 0 + + if test -d $screenshots_path + if test -e $target_path/screenshots + set -a collisions $screenshots_path + else + set screenshots_movable 1 + end + end + + for entry in $top_level_entries + if test -e $target_path/(path basename $entry) + set -a collisions $entry + else + set -a movable_entries $entry + end + end + + set -l movable_count (count $movable_entries) + set -l entry_word entries + test $movable_count -eq 1 + and set entry_word entry + + if set -q _flag_dry_run + if test $screenshots_movable -eq 1 + echo "dot setup folders: would move $screenshots_path to $target_path/screenshots" + end + if test $movable_count -gt 0 + echo "dot setup folders: would move $movable_count $entry_word from ~/$legacy_name to ~/$target_rel" + end + if test (count $collisions) -gt 0 + echo "dot setup folders: ~/$legacy_name has entries already present in ~/$target_rel, would skip (not overwritten):" + for c in $collisions + echo " $c" + end + echo "dot setup folders: ~/$legacy_name would remain in place due to the collision(s) above" + end + continue + end + + if test $screenshots_movable -eq 1 + mv -n $screenshots_path $target_path/screenshots + echo "dot setup folders: moved $screenshots_path to $target_path/screenshots" + end + + if test $movable_count -gt 0 + mv -n $movable_entries $target_path/ + echo "dot setup folders: moved $movable_count $entry_word from ~/$legacy_name to ~/$target_rel" + end + + if test (count $collisions) -gt 0 + echo "dot setup folders: ~/$legacy_name has entries already present in ~/$target_rel, skipping (not overwritten):" + for c in $collisions + echo " $c" + end + echo "dot setup folders: leaving ~/$legacy_name in place due to the collision(s) above" + else + rmdir $legacy_path + end + end + + if not set -q _flag_dry_run + xdg-user-dirs-update + end +end diff --git a/.config/dot/commands/setup/setup.fish b/.config/dot/commands/setup/setup.fish new file mode 100644 index 0000000..0bdf813 --- /dev/null +++ b/.config/dot/commands/setup/setup.fish @@ -0,0 +1,35 @@ +function _dot_setup_usage + echo "usage: dot setup [] + +Tasks: + folders bring the 8 standard XDG user directories under the short-name convention + help show this message + +Run 'dot setup help' for details on a specific task. + +With no task given, runs every setup task." +end + +function _dot_setup + if test "$argv[1]" = help + _dot_setup_usage + return 0 + end + + set -l helper_dir (status dirname) + source $helper_dir/folders.fish + + if test -z "$argv[1]" + _dot_setup_folders + return $status + end + + switch $argv[1] + case folders + _dot_setup_folders $argv[2..-1] + return $status + case '*' + _dot_setup_usage + return 1 + end +end diff --git a/.config/dot/tests/dot.fish b/.config/dot/tests/dot.fish index 53e760d..16a1cd9 100644 --- a/.config/dot/tests/dot.fish +++ b/.config/dot/tests/dot.fish @@ -568,3 +568,360 @@ set -l complete_output (python3 $HOME/.config/dot/commands/kde/kde.py complete) # --- dot help / dot help discovers dot kde --- set -l help_with_kde (dot help) @test "dot help lists the kde subcommand" (string match -q '*kde*' -- $help_with_kde; echo $status) -eq 0 + +# --- dot setup folders --- +# The legacy->short-name mapping is fixed in the command itself, not read +# from ~/.config/user-dirs.dirs (a separate, manually tracked dotfile this +# command never reads or writes), so no scenario below needs to seed one. + +# xdg-user-dirs-update is faked out via a PATH-prepended bin that logs each +# invocation, exactly mirroring dot install's fake sudo/pacman. +set -l fake_bin_xdg (mktemp -d) +echo '#!/bin/sh +echo "$@" >>"$XDG_UPDATE_LOG" +exit 0' >$fake_bin_xdg/xdg-user-dirs-update +chmod +x $fake_bin_xdg/xdg-user-dirs-update +set -gx PATH $fake_bin_xdg $PATH + +# --- a fresh migration: all 8 legacy folders present and empty, including a +# nested empty Pictures/Screenshots --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Desktop $HOME/Documents $HOME/Downloads $HOME/Music $HOME/Pictures/Screenshots $HOME/Videos $HOME/Templates $HOME/Public +set -gx XDG_UPDATE_LOG (mktemp) + +dot setup folders >/dev/null 2>&1 +set -l fresh_folders_status $status + +@test "dot setup folders succeeds on a fresh scratch HOME" $fresh_folders_status -eq 0 +@test "Desktop is renamed to .desktop" -d $HOME/.desktop +@test "Documents is renamed to doc" -d $HOME/doc +@test "Downloads is renamed to dwn" -d $HOME/dwn +@test "Music is renamed to mus" -d $HOME/mus +@test "Pictures is renamed to pic" -d $HOME/pic +@test "Videos is renamed to vid" -d $HOME/vid +@test "Templates and Public both merge into .ignoreme" -d $HOME/.ignoreme +@test "the nested Screenshots folder is renamed to pic/screenshots" -d $HOME/pic/screenshots +@test "the legacy Desktop folder no longer exists" (test -e $HOME/Desktop; and echo yes; or echo no) = no +@test "the legacy Documents folder no longer exists" (test -e $HOME/Documents; and echo yes; or echo no) = no +@test "the legacy Pictures folder no longer exists" (test -e $HOME/Pictures; and echo yes; or echo no) = no +@test "xdg-user-dirs-update is invoked exactly once" (cat $XDG_UPDATE_LOG | count) -eq 1 + +# --- re-running after a clean migration is a no-op --- +dot setup folders >/dev/null 2>&1 +set -l rerun_status $status + +@test "re-running dot setup folders succeeds" $rerun_status -eq 0 +@test "re-running leaves the short-named folders in place" -d $HOME/pic/screenshots +@test "re-running does not recreate any legacy folder" (test -e $HOME/Pictures; and echo yes; or echo no) = no + +# --- the short-name mapping is fixed regardless of what (if anything) +# ~/.config/user-dirs.dirs declares -- this is the exact real-world bug +# that motivated dropping the dependency: a stale, never-updated +# user-dirs.dirs (still pointing XDG_DOCUMENTS_DIR at ~/Documents itself) +# must not make the target collide with the legacy folder --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/.config +echo 'XDG_DESKTOP_DIR="$HOME/Desktop" +XDG_DOWNLOAD_DIR="$HOME/Downloads" +XDG_TEMPLATES_DIR="$HOME/" +XDG_PUBLICSHARE_DIR="$HOME/" +XDG_DOCUMENTS_DIR="$HOME/Documents" +XDG_MUSIC_DIR="$HOME/" +XDG_PICTURES_DIR="$HOME/Pictures" +XDG_VIDEOS_DIR="$HOME/Videos"' >$HOME/.config/user-dirs.dirs +mkdir -p $HOME/Documents +echo real-content >$HOME/Documents/report.txt +set -gx XDG_UPDATE_LOG (mktemp) + +dot setup folders >/dev/null 2>&1 + +@test "a stale user-dirs.dirs pointing at the legacy folder itself doesn't confuse the migration" (cat $HOME/doc/report.txt) = real-content +@test "the legacy folder is still removed despite the stale user-dirs.dirs" (test -e $HOME/Documents; and echo yes; or echo no) = no +@test "the stale user-dirs.dirs file itself is left byte-for-byte untouched" (string match -q '*XDG_DOCUMENTS_DIR="$HOME/Documents"*' -- (cat $HOME/.config/user-dirs.dirs); echo $status) -eq 0 + +# --- dot setup folders works even when user-dirs.dirs doesn't exist at all --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Documents +echo real-content >$HOME/Documents/report.txt +set -gx XDG_UPDATE_LOG (mktemp) + +set -l no_user_dirs_status +dot setup folders >/dev/null 2>&1 +set no_user_dirs_status $status + +@test "dot setup folders succeeds with no user-dirs.dirs present at all" $no_user_dirs_status -eq 0 +@test "migration still happens with no user-dirs.dirs present at all" (cat $HOME/doc/report.txt) = real-content +@test "no user-dirs.dirs is created as a side effect" (test -e $HOME/.config/user-dirs.dirs; and echo yes; or echo no) = no + +# --- bare `dot setup` (no task given) runs folders as part of running everything --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Desktop $HOME/Documents $HOME/Downloads $HOME/Music $HOME/Pictures $HOME/Videos $HOME/Templates $HOME/Public +set -gx XDG_UPDATE_LOG (mktemp) + +dot setup >/dev/null 2>&1 +set -l bare_setup_status $status + +@test "bare dot setup succeeds" $bare_setup_status -eq 0 +@test "bare dot setup runs the folders task" -d $HOME/.desktop +@test "bare dot setup also merges Pictures into pic" -d $HOME/pic + +# --- a non-empty legacy folder merges unconditionally, no flag needed -- +# an unrelated empty legacy folder migrates in the same run --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Documents $HOME/Desktop +echo real-content >$HOME/Documents/report.txt +set -gx XDG_UPDATE_LOG (mktemp) + +set -l nonempty_output (dot setup folders 2>&1) +set -l nonempty_status $status + +@test "dot setup folders succeeds when a legacy folder has content" $nonempty_status -eq 0 +@test "a non-empty legacy folder's content is migrated by default" (cat $HOME/doc/report.txt) = real-content +@test "the now-empty legacy folder is removed" (test -e $HOME/Documents; and echo yes; or echo no) = no +@test "prints a message about what was moved" (string match -q '*Documents*' -- $nonempty_output; echo $status) -eq 0 +@test "an unrelated empty legacy folder still migrates in the same run" (test -e $HOME/Desktop; and echo yes; or echo no) = no + +# --- a legacy folder containing only a stray dotfile still migrates by +# default -- there's no separate empty-vs-non-empty gate to trip --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Downloads +touch $HOME/Downloads/.directory +set -gx XDG_UPDATE_LOG (mktemp) + +dot setup folders >/dev/null 2>&1 + +@test "a legacy folder holding only a stray dotfile is migrated by default" -e $HOME/dwn/.directory +@test "the legacy folder holding only a stray dotfile is removed" (test -e $HOME/Downloads; and echo yes; or echo no) = no + +# --- a nested empty Screenshots folder migrates alongside unrelated real +# content in the same Pictures folder, all in the same default run --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Pictures/Screenshots +echo vacation-photo >$HOME/Pictures/vacation.jpg +set -gx XDG_UPDATE_LOG (mktemp) + +dot setup folders >/dev/null 2>&1 +set -l pictures_with_content_status $status + +@test "dot setup folders succeeds when Pictures has unrelated content" $pictures_with_content_status -eq 0 +@test "the unrelated file in Pictures is migrated into pic" (cat $HOME/pic/vacation.jpg) = vacation-photo +@test "the nested Screenshots folder is renamed to pic/screenshots" -d $HOME/pic/screenshots +@test "the now-empty Pictures folder is removed" (test -e $HOME/Pictures; and echo yes; or echo no) = no + +# --- a Screenshots folder that itself holds real content migrates by +# default too, renamed to pic/screenshots, even when the rest of +# Pictures is empty --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Pictures/Screenshots +echo shot-content >$HOME/Pictures/Screenshots/shot.png +set -gx XDG_UPDATE_LOG (mktemp) + +dot setup folders >/dev/null 2>&1 + +@test "a non-empty Screenshots folder migrates by default, renamed to pic/screenshots" (cat $HOME/pic/screenshots/shot.png) = shot-content +@test "the now-empty Pictures folder is removed after migrating Screenshots" (test -e $HOME/Pictures; and echo yes; or echo no) = no + +# --- a filename collision between a legacy folder and its already-populated +# short-named target is skipped (not overwritten), reported, and leaves +# the legacy folder in place -- even when another non-colliding file in +# the same folder is merged successfully --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Documents $HOME/doc +echo legacy-content >$HOME/Documents/report.txt +echo legacy-only >$HOME/Documents/notes.txt +echo target-content >$HOME/doc/report.txt +set -gx XDG_UPDATE_LOG (mktemp) + +set -l collision_output (dot setup folders 2>&1) +set -l collision_status $status + +@test "dot setup folders still succeeds when a collision occurs" $collision_status -eq 0 +@test "the colliding target file is preserved byte-for-byte" (cat $HOME/doc/report.txt) = target-content +@test "the colliding legacy file is left in place, untouched" (cat $HOME/Documents/report.txt) = legacy-content +@test "the collision is reported" (string match -q '*report.txt*' -- $collision_output; echo $status) -eq 0 +@test "the legacy Documents folder is left in place due to the collision" -d $HOME/Documents +@test "a non-colliding file in the same folder is still merged" (cat $HOME/doc/notes.txt) = legacy-only +@test "the merged non-colliding file no longer sits in the legacy folder" (test -e $HOME/Documents/notes.txt; and echo yes; or echo no) = no + +# --- re-running after a collision was reported: the skipped file isn't +# lost, and the already-migrated file isn't moved again --- +dot setup folders >/dev/null 2>&1 + +@test "re-running after a collision still preserves the target file" (cat $HOME/doc/report.txt) = target-content +@test "re-running after a collision still leaves the legacy file in place" (cat $HOME/Documents/report.txt) = legacy-content +@test "re-running after a collision does not resurrect the already-migrated file in the legacy folder" (test -e $HOME/Documents/notes.txt; and echo yes; or echo no) = no + +# --- a collision on the nested Screenshots unit is skipped, reported, and +# leaves Pictures in place, even though Pictures also holds other +# content unrelated to the collision --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Pictures/Screenshots $HOME/pic/screenshots +echo legacy-shot >$HOME/Pictures/Screenshots/shot.png +echo target-shot >$HOME/pic/screenshots/shot.png +set -gx XDG_UPDATE_LOG (mktemp) + +set -l screenshots_collision_output (dot setup folders 2>&1) + +@test "a Screenshots collision preserves the existing target screenshot" (cat $HOME/pic/screenshots/shot.png) = target-shot +@test "a Screenshots collision leaves the legacy Screenshots folder in place" (cat $HOME/Pictures/Screenshots/shot.png) = legacy-shot +@test "the Screenshots collision is reported" (string match -q '*Screenshots*' -- $screenshots_collision_output; echo $status) -eq 0 +@test "Pictures itself is left in place due to the Screenshots collision" -d $HOME/Pictures + +# --- the same Screenshots-collision handling also holds when Pictures has +# nothing else in it besides the colliding Screenshots folder --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Pictures/Screenshots $HOME/pic/screenshots +echo target-shot >$HOME/pic/screenshots/shot.png +set -gx XDG_UPDATE_LOG (mktemp) + +set -l bare_screenshots_collision_output (dot setup folders 2>&1) +set -l bare_screenshots_collision_status $status + +@test "a Screenshots-only collision still succeeds" $bare_screenshots_collision_status -eq 0 +@test "a Screenshots-only collision preserves the existing target screenshot" (cat $HOME/pic/screenshots/shot.png) = target-shot +@test "a Screenshots-only collision leaves the empty legacy Screenshots folder in place" -d $HOME/Pictures/Screenshots +@test "a Screenshots-only collision leaves Pictures itself in place" -d $HOME/Pictures +@test "the Screenshots-only collision is reported" (string match -q '*Screenshots*' -- $bare_screenshots_collision_output; echo $status) -eq 0 + +# --- --dry-run reports what would move/skip without touching the +# filesystem: no mkdir, no mv/rmdir, no xdg-user-dirs-update --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Documents $HOME/Pictures/Screenshots +echo real-content >$HOME/Documents/report.txt +set -gx XDG_UPDATE_LOG (mktemp) + +set -l dryrun_output (dot setup folders --dry-run 2>&1) +set -l dryrun_status $status + +@test "dot setup folders --dry-run succeeds" $dryrun_status -eq 0 +@test "--dry-run reports the entry it would move" (string match -q '*Documents*' -- $dryrun_output; echo $status) -eq 0 +@test "--dry-run reports the Screenshots folder it would move" (string match -q '*Screenshots*' -- $dryrun_output; echo $status) -eq 0 +@test "--dry-run leaves the legacy Documents folder's content untouched" (cat $HOME/Documents/report.txt) = real-content +@test "--dry-run does not remove the legacy Documents folder" -d $HOME/Documents +@test "--dry-run does not create the short-named target folder" (test -e $HOME/doc; and echo yes; or echo no) = no +@test "--dry-run does not rename the nested Screenshots folder" -d $HOME/Pictures/Screenshots +@test "--dry-run never invokes xdg-user-dirs-update" (test -s $XDG_UPDATE_LOG; and echo yes; or echo no) = no + +# --- --dry-run also reports a would-be collision without touching +# either side, and doesn't move the non-colliding entry either --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Documents $HOME/doc +echo legacy-content >$HOME/Documents/report.txt +echo legacy-only >$HOME/Documents/notes.txt +echo target-content >$HOME/doc/report.txt +set -gx XDG_UPDATE_LOG (mktemp) + +set -l dryrun_collision_output (dot setup folders --dry-run 2>&1) + +@test "--dry-run reports the would-be collision" (string match -q '*report.txt*' -- $dryrun_collision_output; echo $status) -eq 0 +@test "--dry-run leaves the colliding target file untouched" (cat $HOME/doc/report.txt) = target-content +@test "--dry-run leaves the colliding legacy file untouched" (cat $HOME/Documents/report.txt) = legacy-content +@test "--dry-run does not move the non-colliding file either" -e $HOME/Documents/notes.txt + +# --- a legacy folder with nothing to move produces no --dry-run output --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Desktop +set -gx XDG_UPDATE_LOG (mktemp) + +set -l dryrun_empty_output (dot setup folders --dry-run 2>&1) + +@test "--dry-run is silent for a legacy folder with nothing to move" -z "$dryrun_empty_output" +@test "--dry-run leaves an empty legacy folder in place" -d $HOME/Desktop + +# --- the removed --yes flag now fails fast as an unknown option --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +set -gx XDG_UPDATE_LOG (mktemp) + +dot setup folders --yes >/dev/null 2>&1 +set -l old_yes_status $status + +@test "dot setup folders --yes now fails as an unknown option" $old_yes_status -ne 0 + +# --- help prints usage and makes no filesystem changes --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands/setup +cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish +cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish +mkdir -p $HOME/Documents $HOME/Desktop +set -gx XDG_UPDATE_LOG (mktemp) + +set -l setup_help_output (dot setup help) +set -l setup_help_status $status +set -l folders_help_output (dot setup folders help) +set -l folders_help_status $status +set -l xdg_called_for_help (test -s $XDG_UPDATE_LOG; and echo yes; or echo no) + +@test "dot setup help succeeds" $setup_help_status -eq 0 +@test "dot setup help mentions folders" (string match -q '*folders*' -- $setup_help_output; echo $status) -eq 0 +@test "dot setup folders help succeeds" $folders_help_status -eq 0 +@test "dot setup folders help mentions the short-name convention" (string match -q '*.desktop*' -- $folders_help_output; echo $status) -eq 0 +@test "dot setup folders help documents --dry-run" (string match -q '*--dry-run*' -- $folders_help_output; echo $status) -eq 0 +@test "neither help invocation ever calls xdg-user-dirs-update" $xdg_called_for_help = no +@test "dot setup help leaves the legacy Documents folder untouched" -d $HOME/Documents +@test "dot setup folders help leaves the legacy Desktop folder untouched" -d $HOME/Desktop +@test "help does not create any short-named target folder" (test -e $HOME/doc; and echo yes; or echo no) = no + +# --- dot help discovers dot setup --- +set -l help_with_setup (dot help) +@test "dot help lists the setup subcommand" (string match -q '*setup*' -- $help_with_setup; echo $status) -eq 0 diff --git a/.config/fish/completions/dot.fish b/.config/fish/completions/dot.fish index 4fdf2cf..d686b0b 100644 --- a/.config/fish/completions/dot.fish +++ b/.config/fish/completions/dot.fish @@ -18,6 +18,11 @@ complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_arg complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_argument -l no-sync" -l no-sync -d "skip the pacman -Sy database refresh" complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_argument -l restore" -f -a "(__fish_print_pacman_packages)" +# --- dot setup --- +complete -c dot -n "__fish_seen_subcommand_from setup; and not __fish_seen_subcommand_from folders help" -f -a folders -d "bring the 8 standard XDG user directories under the short-name convention" +complete -c dot -n "__fish_seen_subcommand_from setup; and not __fish_seen_subcommand_from folders help" -f -a help -d "show usage" +complete -c dot -n "__fish_seen_subcommand_from setup; and __fish_seen_subcommand_from folders" -f -a help -d "show usage" + # --- dot kde --- complete -c dot -n "__fish_seen_subcommand_from kde; and not __fish_seen_subcommand_from apply diff save help" -f -a apply -d "push manifest entries onto the live system" complete -c dot -n "__fish_seen_subcommand_from kde; and not __fish_seen_subcommand_from apply diff save help" -f -a diff -d "scan for settings whose live value differs from its default" diff --git a/.config/user-dirs.dirs b/.config/user-dirs.dirs new file mode 100644 index 0000000..1b5a3ff --- /dev/null +++ b/.config/user-dirs.dirs @@ -0,0 +1,16 @@ +# This file is written by xdg-user-dirs-update +# If you want to change or add directories, just edit the line you're +# interested in. All local changes will be retained on the next run. +# Format is XDG_xxx_DIR="$HOME/yyy", where yyy is a shell-escaped +# homedir-relative path, or XDG_xxx_DIR="/yyy", where /yyy is an +# absolute path. No other format is supported. +# +XDG_DESKTOP_DIR="$HOME/.desktop" +XDG_DOWNLOAD_DIR="$HOME/dwn" +XDG_TEMPLATES_DIR="$HOME/.ignoreme" +XDG_PUBLICSHARE_DIR="$HOME/.ignoreme" +XDG_DOCUMENTS_DIR="$HOME/doc" +XDG_MUSIC_DIR="$HOME/mus" +XDG_PICTURES_DIR="$HOME/pic" +XDG_VIDEOS_DIR="$HOME/vid" +XDG_PROJECTS_DIR="$HOME/wrk" diff --git a/.github/README.md b/.github/README.md index e444998..b9a6dcf 100644 --- a/.github/README.md +++ b/.github/README.md @@ -25,6 +25,8 @@ fish -c 'dot init' | `dot kde help` | Lists `dot kde`'s subcommands. | | `dot kde save ` | Reads a KDE setting's current live value and declares it in the manifest (`~/.config/dot/kde-manifest`). | | `dot kde save` | Refreshes every already-declared manifest entry's value from the live system. | +| `dot setup` | Runs every machine-setup task (currently just `folders`). | +| `dot setup folders` | Brings the 8 standard XDG user directories (`~/Desktop`, `~/Documents`, ...) under a fixed short-name convention (`~/.desktop`, `~/doc`, ...). | | `dot ` | Everything else is passed to `git`. | See [CLAUDE.md](../.config/dot/CLAUDE.md) for the `dot` tool's internal