aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-06-16 18:29:54 +0200
committerDanilo M. <danix@danix.xyz>2026-06-16 18:29:54 +0200
commitcaf279a0fc72e0897bfeaa91b62eea9600b43a62 (patch)
tree028ddc288f9b9c4f329398915b01db6ae1175d49 /docs
parent3db4c01ea811d72dbd287b49bf5267b531ec8908 (diff)
downloaddots-backup-caf279a0fc72e0897bfeaa91b62eea9600b43a62.tar.gz
dots-backup-caf279a0fc72e0897bfeaa91b62eea9600b43a62.zip
docs: implementation plan for --suggest flag
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/plans/2026-06-16-suggest-flag.md482
1 files changed, 482 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-06-16-suggest-flag.md b/docs/superpowers/plans/2026-06-16-suggest-flag.md
new file mode 100644
index 0000000..6a7c968
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-16-suggest-flag.md
@@ -0,0 +1,482 @@
+# `--suggest` Flag Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add a read-only `-s|--suggest` flag to `dot-backup.sh` that lists untracked config dirs/files created since the last backup and prints a paste-ready `files.list` block.
+
+**Architecture:** A new `do_suggest()` function scans `~/.config/*`, `~/.*` dirs, and `~/.*` files one level deep, filters by mtime-vs-last-backup, dedup against `DOTFILES`, and a `SUGGEST_IGNORE` basename skip list. The `lastupdate()` function gains a machine-readable `lastupdate.epoch` sibling file for the mtime comparison. Suggest dispatches early (after the FIRST_RUN guard, before restore/backup) and always writes to real stdout, bypassing `--quiet`.
+
+**Tech Stack:** Pure bash, coreutils (`date`, `stat`, `sort`). No test framework in repo — verification is manual via documented commands.
+
+**Spec:** `docs/superpowers/specs/2026-06-16-suggest-flag-design.md`
+
+**Note on TDD:** This repo has no test harness (CLAUDE.md: "No tests"). Each task uses manual verification commands with expected output in place of automated tests. Run them exactly; they are the acceptance check.
+
+---
+
+## File Structure
+
+- `dot-backup.sh` — all logic. New flag var, arg case, usage line; quiet-redirect guard; `lastupdate()` modification; `SUGGEST_IGNORE` default; `do_suggest()` function; early dispatch.
+- `config.example` — commented `SUGGEST_IGNORE` example.
+- `README.md` — document `--suggest` in usage/options.
+- `CLAUDE.md` — note the flag and `lastupdate.epoch`.
+
+All current line numbers are from the working tree at plan time and may drift as edits land — match on the quoted code, not the number.
+
+---
+
+## Task 1: Flag plumbing (var, arg case, usage)
+
+**Files:**
+- Modify: `dot-backup.sh` (flag block ~line 49-54, `usage()` ~line 56-65, arg loop ~line 67-77)
+
+- [ ] **Step 1: Add the flag variable**
+
+In the flags block, after `PUSH=false`, add:
+
+```bash
+SUGGEST=false
+```
+
+Result block:
+
+```bash
+# Flags
+DRY_RUN=false
+VERBOSE=false
+RESTORE=false
+QUIET=false
+PUSH=false
+SUGGEST=false
+```
+
+- [ ] **Step 2: Add the usage line**
+
+In `usage()`, after the `--push` line and before `--help`, add:
+
+```bash
+ echo " -s, --suggest List untracked config dirs/files new since last backup, then exit"
+```
+
+- [ ] **Step 3: Add the arg case**
+
+In the `for arg in "$@"` case statement, after the `-p|--push` line, add:
+
+```bash
+ -s|--suggest) SUGGEST=true ;;
+```
+
+- [ ] **Step 4: Verify the flag parses and help shows it**
+
+Run: `bash -n dot-backup.sh && ./dot-backup.sh --help`
+Expected: no syntax error; help output includes the `-s, --suggest` line.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add dot-backup.sh
+git commit -m "feat: add --suggest flag plumbing"
+```
+
+---
+
+## Task 2: Guard the quiet redirect so suggest stays on stdout
+
+**Files:**
+- Modify: `dot-backup.sh` (quiet redirect block ~line 79-84)
+
+- [ ] **Step 1: Update the quiet guard**
+
+Change:
+
+```bash
+if [[ "$QUIET" == true ]]; then
+```
+
+to:
+
+```bash
+if [[ "$QUIET" == true && "$SUGGEST" == false ]]; then
+```
+
+The rest of the block (fd 3 save, redirect) is unchanged. `SUGGEST` is parsed in the arg loop above this block, so it is known here.
+
+- [ ] **Step 2: Verify redirect is skipped under suggest**
+
+Run: `./dot-backup.sh --suggest --quiet 2>&1 | head -1`
+Expected: output appears on the terminal (not silently swallowed to log). Exact content is implemented in Task 5; for now it may error on a not-yet-defined function — that is fine, the point is stdout is NOT redirected. If `do_suggest` is undefined at this stage, expect a "command not found" on stdout, which still proves the redirect was skipped.
+
+> Note: if running tasks strictly in order, `do_suggest` does not exist yet. You may defer this verification to after Task 5, or temporarily observe the "command not found" lands on the terminal. Either confirms the guard.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add dot-backup.sh
+git commit -m "feat: suggest bypasses --quiet redirect"
+```
+
+---
+
+## Task 3: Write `lastupdate.epoch` alongside human timestamp
+
+**Files:**
+- Modify: `dot-backup.sh` (`lastupdate()` ~line 184-187)
+
+- [ ] **Step 1: Modify `lastupdate()`**
+
+Change:
+
+```bash
+lastupdate() {
+ D=$(date)
+ echo "Last update: $D" > "${DEFAULT_OUTPUT_DIR}/lastupdate"
+}
+```
+
+to:
+
+```bash
+lastupdate() {
+ D=$(date)
+ echo "Last update: $D" > "${DEFAULT_OUTPUT_DIR}/lastupdate"
+ date +%s > "${DEFAULT_OUTPUT_DIR}/lastupdate.epoch"
+}
+```
+
+- [ ] **Step 2: Verify both files written by a dry-run-free path**
+
+`lastupdate()` is only called in the real backup flow (not dry-run). Verify in isolation:
+
+Run:
+```bash
+DEFAULT_OUTPUT_DIR=$(mktemp -d); export DEFAULT_OUTPUT_DIR
+bash -c 'source <(sed -n "184,188p" dot-backup.sh); DEFAULT_OUTPUT_DIR="'"$DEFAULT_OUTPUT_DIR"'"; lastupdate; cat "$DEFAULT_OUTPUT_DIR/lastupdate"; echo "epoch=$(cat "$DEFAULT_OUTPUT_DIR/lastupdate.epoch")"'
+```
+Expected: prints `Last update: <date>` and `epoch=<10-digit integer>`.
+(If the `sed` line range drifted, adjust to the `lastupdate()` definition lines.)
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add dot-backup.sh
+git commit -m "feat: lastupdate also writes machine-readable epoch"
+```
+
+---
+
+## Task 4: Add `SUGGEST_IGNORE` default
+
+**Files:**
+- Modify: `dot-backup.sh` (after config source ~line 47, before flags block)
+
+- [ ] **Step 1: Add the guarded default**
+
+After the config-file `source` block (line ~44-47) and before the `# Flags` block, add:
+
+```bash
+# Basenames skipped by --suggest. User may override in config.
+if [[ -z "${SUGGEST_IGNORE+set}" ]]; then
+ SUGGEST_IGNORE=(.cache .local .git .ssh .gnupg .Trash .pki .nv \
+ .mozilla .thunderbird .npm .cargo .rustup .java \
+ .dbus .config)
+fi
+```
+
+The `${SUGGEST_IGNORE+set}` test is empty only when the variable is wholly
+unset, so a user who defines an explicit (even empty) array in config is
+respected.
+
+- [ ] **Step 2: Verify default populates and config override wins**
+
+Run (default):
+```bash
+bash -c 'source <(sed -n "/Basenames skipped/,/^fi$/p" dot-backup.sh); printf "%s\n" "${SUGGEST_IGNORE[@]}"' | grep -c '^\.'
+```
+Expected: `16` (count of default entries).
+
+Run (override respected):
+```bash
+bash -c 'SUGGEST_IGNORE=(.foo); source <(sed -n "/Basenames skipped/,/^fi$/p" dot-backup.sh); printf "%s\n" "${SUGGEST_IGNORE[@]}"'
+```
+Expected: prints only `.foo` (default did not overwrite).
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add dot-backup.sh
+git commit -m "feat: add SUGGEST_IGNORE default skip list"
+```
+
+---
+
+## Task 5: Implement `do_suggest()`
+
+**Files:**
+- Modify: `dot-backup.sh` (add function near other functions, after `do_rsync()` ~line 216, before RESTORE MODE section ~line 218)
+
+- [ ] **Step 1: Add the `do_suggest()` function**
+
+Insert after the closing `}` of `do_rsync()` and before the `# ── RESTORE MODE` comment:
+
+```bash
+# ── SUGGEST MODE ──────────────────────────────────────────────────────────────
+
+# True if candidate path $1 is already covered by any DOTFILES entry
+# (exact match, parent of, or child of an existing entry).
+_suggest_covered() {
+ local c="$1" e
+ for e in "${DOTFILES[@]}"; do
+ # normalize trailing slash
+ e="${e%/}"
+ [[ "$c" == "$e" ]] && return 0
+ [[ "$c" == "$e"/* ]] && return 0
+ [[ "$e" == "$c"/* ]] && return 0
+ done
+ return 1
+}
+
+# True if basename $1 is in SUGGEST_IGNORE
+_suggest_ignored() {
+ local b="$1" ig
+ for ig in "${SUGGEST_IGNORE[@]}"; do
+ [[ "$b" == "$ig" ]] && return 0
+ done
+ return 1
+}
+
+do_suggest() {
+ local epoch_file="${DEFAULT_OUTPUT_DIR}/lastupdate.epoch"
+ local last_epoch=0
+ local last_human="never"
+ if [[ -r "$epoch_file" ]]; then
+ local raw
+ raw="$(cat "$epoch_file")"
+ if [[ "$raw" =~ ^[0-9]+$ ]]; then
+ last_epoch="$raw"
+ last_human="$(date -d "@$last_epoch" 2>/dev/null || echo "$last_epoch")"
+ fi
+ fi
+
+ local -a candidates=()
+ local path emit base mtime
+
+ # Source 1: ~/.config/* (children of .config)
+ for path in "${HOME}/.config/"*; do
+ [[ -e "$path" ]] || continue
+ base="$(basename "$path")"
+ emit=".config/${base}"
+ _suggest_ignored "$base" && continue
+ _suggest_covered "$emit" && continue
+ mtime="$(stat -c %Y "$path" 2>/dev/null || echo 0)"
+ (( mtime > last_epoch )) && candidates+=("$emit")
+ done
+
+ # Source 2 + 3: ~/.* (hidden dirs and files at home top level)
+ for path in "${HOME}/".*; do
+ base="$(basename "$path")"
+ [[ "$base" == "." || "$base" == ".." ]] && continue
+ [[ -d "$path" || -f "$path" ]] || continue
+ emit="${base}"
+ _suggest_ignored "$base" && continue
+ _suggest_covered "$emit" && continue
+ mtime="$(stat -c %Y "$path" 2>/dev/null || echo 0)"
+ (( mtime > last_epoch )) && candidates+=("$emit")
+ done
+
+ if [[ ${#candidates[@]} -eq 0 ]]; then
+ echo -e "${GREEN}No new config dirs/files since last backup.${NC}"
+ return 0
+ fi
+
+ # sort alphabetically, stable output
+ local -a sorted=()
+ while IFS= read -r line; do sorted+=("$line"); done < <(printf '%s\n' "${candidates[@]}" | sort -u)
+
+ echo -e "${GREEN}New since last backup (${last_human}):${NC}"
+ for emit in "${sorted[@]}"; do
+ echo -e " ${BLUE}${emit}${NC}"
+ done
+ echo
+ echo "Paste into files.list:"
+ for emit in "${sorted[@]}"; do
+ echo "$emit"
+ done
+}
+```
+
+- [ ] **Step 2: Verify syntax**
+
+Run: `bash -n dot-backup.sh`
+Expected: no output (clean parse).
+
+- [ ] **Step 3: Verify the function runs standalone**
+
+The function is not yet dispatched (Task 6). Smoke-test the parse only here; full behavior is verified in Task 6 once wired up.
+
+Run: `bash -n dot-backup.sh && echo OK`
+Expected: `OK`.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add dot-backup.sh
+git commit -m "feat: add do_suggest scan/filter/dedup function"
+```
+
+---
+
+## Task 6: Dispatch `--suggest` early and exit
+
+**Files:**
+- Modify: `dot-backup.sh` (MAIN section, after FIRST_RUN guard ~line 309, before DRY_RUN/RESTORE ~line 311-317)
+
+- [ ] **Step 1: Add the dispatch block**
+
+After the `FIRST_RUN` `if` block (the one ending `exit 0` / `fi` around line 309) and before the `if [[ "$DRY_RUN" == true ]]` block, add:
+
+```bash
+if [[ "$SUGGEST" == true ]]; then
+ do_suggest
+ exit 0
+fi
+```
+
+Placing it after FIRST_RUN means a brand-new install (no config yet) shows the
+first-run message instead of an empty suggest. Placing it before DRY_RUN and
+RESTORE means suggest takes precedence over those flags, per spec.
+
+- [ ] **Step 2: Verify suggest lists and exits without backing up**
+
+Run: `./dot-backup.sh --suggest`
+Expected: either `New since last backup (...)` with an indented list plus a
+`Paste into files.list:` block, OR `No new config dirs/files since last backup.`
+No `git` commit output, no `Copied` lines — it exited before the backup flow.
+
+- [ ] **Step 3: Verify dedup — a listed path is not suggested**
+
+Create a throwaway config dir and confirm it shows, then suppress it:
+
+```bash
+mkdir -p ~/.config/zzztest
+touch -d "+1 hour" ~/.config/zzztest # ensure mtime newer than last backup
+./dot-backup.sh --suggest | grep zzztest
+```
+Expected: `.config/zzztest` appears.
+
+Now add it to an external list and confirm it disappears:
+```bash
+mkdir -p ~/.config/dot-backup
+echo ".config/zzztest" >> ~/.config/dot-backup/files.list
+./dot-backup.sh --suggest | grep zzztest && echo "STILL THERE (bug)" || echo "deduped OK"
+```
+Expected: `deduped OK`.
+
+Cleanup:
+```bash
+sed -i '/^\.config\/zzztest$/d' ~/.config/dot-backup/files.list
+rmdir ~/.config/zzztest
+```
+(If `files.list` is now empty and was created only for this test, remove it.)
+
+- [ ] **Step 4: Verify ignore list suppresses a basename**
+
+```bash
+mkdir -p ~/.config/zzzignore; touch -d "+1 hour" ~/.config/zzzignore
+./dot-backup.sh --suggest | grep zzzignore
+```
+Expected: `.config/zzzignore` appears (not yet ignored).
+
+Add to config and re-run:
+```bash
+echo 'SUGGEST_IGNORE=(.cache .local .git .ssh .gnupg .Trash .pki .nv .mozilla .thunderbird .npm .cargo .rustup .java .dbus .config zzzignore)' >> ~/.config/dot-backup/config
+./dot-backup.sh --suggest | grep zzzignore && echo "STILL THERE (bug)" || echo "ignored OK"
+```
+Expected: `ignored OK`.
+
+Cleanup: remove the added `SUGGEST_IGNORE=` line from `~/.config/dot-backup/config` and `rmdir ~/.config/zzzignore`.
+
+- [ ] **Step 5: Verify suggest survives `--quiet` (stdout, not log)**
+
+Run: `./dot-backup.sh --suggest --quiet`
+Expected: output appears on terminal (the Task 2 guard works end-to-end). The
+log file is not written for this invocation.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add dot-backup.sh
+git commit -m "feat: dispatch --suggest early and exit"
+```
+
+---
+
+## Task 7: Document in config.example, README, CLAUDE.md
+
+**Files:**
+- Modify: `config.example`
+- Modify: `README.md`
+- Modify: `CLAUDE.md`
+
+- [ ] **Step 1: Add SUGGEST_IGNORE to config.example**
+
+Append to `config.example`:
+
+```bash
+
+# Basenames --suggest skips when scanning for new config (override the
+# baked-in default). Uncomment and edit to customize.
+#SUGGEST_IGNORE=(.cache .local .git .ssh .gnupg .Trash .pki .nv \
+# .mozilla .thunderbird .npm .cargo .rustup .java \
+# .dbus .config)
+```
+
+- [ ] **Step 2: Document the flag in README.md**
+
+Find the options/usage list in `README.md` and add an entry matching the
+existing format for the other flags:
+
+```
+-s, --suggest List untracked config dirs/files new since last backup, then exit
+```
+
+If README has a prose section per flag, add a short paragraph:
+
+> `--suggest` scans `~/.config` children and top-level hidden dirs/files in
+> `$HOME`, and prints any that are newer than the last backup and not already
+> tracked, formatted for pasting into `files.list`. It never copies or commits.
+> Tune what it skips with `SUGGEST_IGNORE` in the config file.
+
+Match the README's actual heading/format; do not invent a new section style.
+
+- [ ] **Step 3: Note the flag and epoch file in CLAUDE.md**
+
+In `CLAUDE.md`, add `--suggest` to the behavior description and note the new
+file. Suggested addition under the architecture/running section:
+
+> `--suggest` lists untracked config dirs/files newer than the last backup
+> (read-only; prints a `files.list` paste block, then exits). The backup writes
+> a machine-readable `lastupdate.epoch` (epoch seconds) next to `lastupdate`;
+> `--suggest` reads it to decide what counts as "new".
+
+- [ ] **Step 4: Verify docs reference the real flag**
+
+Run: `grep -l 'suggest' config.example README.md CLAUDE.md`
+Expected: all three filenames listed.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add config.example README.md CLAUDE.md
+git commit -m "docs: document --suggest flag and SUGGEST_IGNORE"
+```
+
+---
+
+## Final verification
+
+- [ ] `bash -n dot-backup.sh` — clean parse.
+- [ ] `./dot-backup.sh --help` — shows `-s, --suggest`.
+- [ ] `./dot-backup.sh --suggest` — lists or reports none, exits without backup.
+- [ ] `./dot-backup.sh --suggest --quiet` — output on terminal, not log.
+- [ ] A real backup run (`./dot-backup.sh`) writes both `lastupdate` and `lastupdate.epoch` in `$DEFAULT_OUTPUT_DIR`.
+- [ ] After a real backup, `--suggest` list shrinks to only paths newer than that backup.