# Repo setup + update (`--setup-repo` / `--update`) 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 two modes to `sbo-batch-test`: `--setup-repo` (rm+rsync the SBo tree, clone personal subtrees, shadow upstream dups) and `--update` (rsync-with-delete to unshadow, git-pull subtrees, re-shadow) before a build.
**Architecture:** One bash file, `sbo-batch-test`. New config `SBO_RSYNC_URL` (string) and `SBO_PERSONAL_TREES` (assoc array dir->URL). Four new functions: two pure/testable helpers (`collect_shadows`, `rsync_excludes`) and two orchestrators (`setup_repo`, `update_personal_trees`) plus a shared `shadow_scan` and `rsync_tree`. `--setup-repo` is a standalone exit mode like `--init-base`; `--update` runs inline before build. Personal subtrees are excluded from rsync `--delete` so they are never wiped.
**Tech Stack:** bash, rsync, git, `test-logic.sh` self-check (assert-style, runs as normal user).
Spec: `docs/superpowers/specs/2026-07-11-repo-setup-update-design.md`.
---
### Task 1: Config vars (defaults + example + config-block source)
**Files:**
- Modify: `sbo-batch-test:45-55` (CONFIG block defaults + source)
- Modify: `config.example`
- [ ] **Step 1: Add empty defaults to the CONFIG block**
In `sbo-batch-test`, after the `VERSION="15.0"` line (currently line 47) and BEFORE the `SBO_BATCH_CONFIG=` source block (line 49+), add:
```bash
# rsync source for the SBo tree (same source sbopkg uses). --setup-repo rsyncs
# it fresh; --update rsyncs with --delete (personal subtrees excluded) to
# unshadow. Empty defaults; real value in the external config.
SBO_RSYNC_URL=""
# Personal SBo subtrees: dir name (under SBO_TREE_ROOTS[0]) -> git clone URL.
# declare -A here so the external config's assignment lands as a global assoc
# array. --setup-repo clones each; --update git-pulls each; both then shadow.
declare -A SBO_PERSONAL_TREES=()
```
- [ ] **Step 2: Verify the script still parses**
Run: `bash -n sbo-batch-test`
Expected: no output, exit 0.
- [ ] **Step 3: Add the documented vars to config.example**
Append to `config.example` (after the `VERSION="15.0"` line):
```bash
# rsync source for the SBo tree (same source sbopkg uses). --setup-repo rm -rf's
# SBO_TREE_ROOTS[0] and rsyncs this into it fresh; --update rsyncs it with
# --delete (personal subtrees excluded) to refresh upstream and unshadow.
# Empty: --setup-repo refuses to run; --update skips the rsync and only pulls +
# shadows. Package SOURCE for the tree only; unrelated to LOCAL_MIRROR_15 (the
# NFS package mirror).
SBO_RSYNC_URL="slackbuilds.org::slackbuilds/15.0"
# Personal SBo subtrees: directory name (under SBO_TREE_ROOTS[0]) -> git clone
# URL. --setup-repo clones each after the rsync; --update git-pulls each in
# place. Both then shadow any same-named upstream package found elsewhere in the
# tree (the personal version wins, the upstream duplicate is rm -r'd). Empty =
# no personal trees.
declare -A SBO_PERSONAL_TREES=(
[personal]="https://github.com/danixland/my-slackbuilds.git"
[pentesting]="https://github.com/danixland/Slackware-Pentesting-Suite.git"
)
```
- [ ] **Step 4: Commit**
```bash
git add sbo-batch-test config.example
git commit -S -m "Add SBO_RSYNC_URL and SBO_PERSONAL_TREES config vars"
```
---
### Task 2: `rsync_excludes` helper (pure, tested)
**Files:**
- Modify: `sbo-batch-test` (new function near the other tree helpers, before `main`)
- Test: `test-logic.sh`
- [ ] **Step 1: Write the failing test**
Append to `test-logic.sh` BEFORE the `# --- result` block:
```bash
# --- rsync_excludes: one anchored --exclude per personal tree key ------------
declare -A SBO_PERSONAL_TREES=([personal]=url1 [pentesting]=url2)
ex="$(rsync_excludes)"
echo "$ex" | grep -qx -- "--exclude=/personal/" && ok "rsync_excludes personal" || bad "missing --exclude=/personal/, got [$ex]"
echo "$ex" | grep -qx -- "--exclude=/pentesting/" && ok "rsync_excludes pentesting" || bad "missing --exclude=/pentesting/, got [$ex]"
declare -A SBO_PERSONAL_TREES=()
[[ -z "$(rsync_excludes)" ]] && ok "rsync_excludes empty for no trees" || bad "rsync_excludes should be empty, got [$(rsync_excludes)]"
```
- [ ] **Step 2: Run test to verify it fails**
Run: `bash test-logic.sh`
Expected: FAIL, `rsync_excludes: command not found` / a `bad` line.
- [ ] **Step 3: Write minimal implementation**
In `sbo-batch-test`, add before `main()` (e.g. just after `update_base`, near line 312):
```bash
# Echo one anchored `--exclude=/
/` per SBO_PERSONAL_TREES key, one per
# line. Anchored to the tree root so rsync --delete never wipes a personal
# clone (they are not on the rsync source). Empty when no personal trees.
rsync_excludes() {
local dir
for dir in "${!SBO_PERSONAL_TREES[@]}"; do
echo "--exclude=/$dir/"
done
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `bash test-logic.sh`
Expected: the three new `ok` lines, no `bad`.
- [ ] **Step 5: Commit**
```bash
git add sbo-batch-test test-logic.sh
git commit -S -m "Add rsync_excludes helper + self-check"
```
---
### Task 3: `rsync_tree` (shared rsync with personal excludes)
**Files:**
- Modify: `sbo-batch-test` (new function after `rsync_excludes`)
No new test: pure I/O against rsync, mirrors the `update_base`/`lint_pkg` no-stub precedent. The branchy piece (excludes) is already tested in Task 2.
- [ ] **Step 1: Write the implementation**
In `sbo-batch-test`, add directly after `rsync_excludes`:
```bash
# rsync the SBo tree from SBO_RSYNC_URL into , excluding every personal
# subtree from transfer AND deletion. Flags match sbopkg's own rsync (files
# root:root). Returns rsync's exit status; caller decides hard-fail vs warn.
rsync_tree() {
local root="$1"
local excludes=()
mapfile -t excludes < <(rsync_excludes)
rsync -a --delete --no-owner --no-group \
"${excludes[@]}" \
"$SBO_RSYNC_URL/" "$root/"
}
```
- [ ] **Step 2: Verify the script still parses**
Run: `bash -n sbo-batch-test`
Expected: no output, exit 0.
- [ ] **Step 3: Commit**
```bash
git add sbo-batch-test
git commit -S -m "Add rsync_tree helper (personal subtrees excluded from --delete)"
```
---
### Task 4: `collect_shadows` (pure dup detection, tested)
**Files:**
- Modify: `sbo-batch-test` (new function after `rsync_tree`)
- Test: `test-logic.sh`
- [ ] **Step 1: Write the failing test**
Append to `test-logic.sh` BEFORE the `# --- result` block:
```bash
# --- collect_shadows: upstream dups of personal packages -------------------
S=$(mktemp -d)
mkslb() { mkdir -p "$S/$1/$2"; : > "$S/$1/$2/$2.SlackBuild"; }
# upstream copies
mkslb academic foo
mkslb network bar
# personal copies (win)
mkslb personal foo
mkslb pentesting bar
# decoy: personal pkg with no upstream twin
mkslb personal nomatch
# decoy: personal subdir without a .SlackBuild (ignored)
mkdir -p "$S/personal/noslb"
declare -A SBO_PERSONAL_TREES=([personal]=u1 [pentesting]=u2)
got="$(collect_shadows "$S" | sort)"
want="$(printf 'academic/foo\nnetwork/bar\n' | sort)"
[[ "$got" == "$want" ]] && ok "collect_shadows finds upstream dups only" || bad "collect_shadows wrong; got [$got] want [$want]"
echo "$got" | grep -q "personal/" && bad "collect_shadows returned a personal path" || ok "collect_shadows excludes personal trees"
echo "$got" | grep -q "nomatch" && bad "collect_shadows invented a match for nomatch" || ok "collect_shadows ignores no-twin pkg"
rm -rf "$S"
```
- [ ] **Step 2: Run test to verify it fails**
Run: `bash test-logic.sh`
Expected: FAIL, `collect_shadows: command not found`.
- [ ] **Step 3: Write minimal implementation**
In `sbo-batch-test`, add after `rsync_tree`. Adapted from
`slackrepo_setup_SBo-danix.sh:38-55`, generalized over `SBO_PERSONAL_TREES` keys:
```bash
# Echo (one per line) the -relative dirs of upstream packages that
# duplicate a package in a personal subtree. Reads only, never removes. A
# personal package is a subdir with a matching .SlackBuild; an upstream
# dup is a same-named depth-2 dir anywhere under outside the personal
# trees.
collect_shadows() {
local root="$1"
local -a personal_dirs=() prune=()
local dir
for dir in "${!SBO_PERSONAL_TREES[@]}"; do
[[ -d "$root/$dir" ]] || continue
personal_dirs+=("$root/$dir")
prune+=(! -path "$root/$dir/*")
done
[[ ${#personal_dirs[@]} -eq 0 ]] && return
local pkgdir pkg match
while IFS= read -r pkgdir; do
pkg=$(basename "$pkgdir")
[[ -f "$pkgdir/$pkg.SlackBuild" ]] || continue
while IFS= read -r match; do
echo "${match#"$root"/}"
done < <(find "$root" -mindepth 2 -maxdepth 2 -type d \
-name "$pkg" "${prune[@]}")
done < <(find "${personal_dirs[@]}" -mindepth 1 -maxdepth 1 -type d)
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `bash test-logic.sh`
Expected: the new `ok` lines, no `bad`, `ALL LOGIC CHECKS PASS`.
- [ ] **Step 5: Commit**
```bash
git add sbo-batch-test test-logic.sh
git commit -S -m "Add collect_shadows dup detection + self-check"
```
---
### Task 5: `shadow_scan` (interactive removal, no test)
**Files:**
- Modify: `sbo-batch-test` (new function after `collect_shadows`)
No new test: `collect_shadows` is the branchy piece and is tested; this wraps it with print + confirm + `rm -r` (I/O + interaction).
- [ ] **Step 1: Write the implementation**
In `sbo-batch-test`, add after `collect_shadows`:
```bash
# Show the upstream dups of personal packages, confirm, and rm -r them. Reads
# the set from collect_shadows. Interactive by design (the user wants to see
# removals before they happen). No git (the 15.0 tree is rsync'd, not git).
shadow_scan() {
local root="$1"
local -a to_shadow=()
mapfile -t to_shadow < <(collect_shadows "$root")
if [[ ${#to_shadow[@]} -eq 0 ]]; then
echo "No upstream packages match your personal subtrees. Nothing to shadow."
return
fi
echo
echo "The following upstream packages will be shadowed (rm -r):"
local item
for item in "${to_shadow[@]}"; do
echo " - $item"
done
echo
read -rp "Proceed? [y/N] " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Aborted shadowing; no packages were removed."
return
fi
for item in "${to_shadow[@]}"; do
echo "Shadowing: $item"
rm -r "${root:?}/$item"
done
}
```
- [ ] **Step 2: Verify the script still parses**
Run: `bash -n sbo-batch-test`
Expected: no output, exit 0.
- [ ] **Step 3: Commit**
```bash
git add sbo-batch-test
git commit -S -m "Add shadow_scan (confirm + rm -r upstream dups)"
```
---
### Task 6: `setup_repo` (standalone exit mode)
**Files:**
- Modify: `sbo-batch-test` (new function after `shadow_scan`)
- [ ] **Step 1: Write the implementation**
In `sbo-batch-test`, add after `shadow_scan`:
```bash
# --setup-repo: first-time full build of the SBo tree. rm -rf the tree root,
# rsync it fresh, clone the personal subtrees in, shadow upstream dups. Runs its
# own checks (the tree may not exist yet). Standalone: exits, never builds.
setup_repo() {
[[ $EUID -eq 0 ]] || { echo "--setup-repo must run as root (rsync/rm to the tree)." >&2; exit 1; }
require_config
[[ -n "$SBO_RSYNC_URL" ]] || { echo "SBO_RSYNC_URL is unset; set it in $SBO_BATCH_CONFIG." >&2; exit 1; }
local root="${SBO_TREE_ROOTS[0]}"
[[ -n "$root" ]] || { echo "SBO_TREE_ROOTS[0] is empty." >&2; exit 1; }
echo "This will REMOVE $root and rsync it fresh from $SBO_RSYNC_URL."
read -rp "Proceed? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
rm -rf "${root:?}"
echo "Rsyncing tree..."
rsync_tree "$root" || { echo "rsync failed." >&2; exit 1; }
local dir
for dir in "${!SBO_PERSONAL_TREES[@]}"; do
echo "Cloning $dir..."
git clone "${SBO_PERSONAL_TREES[$dir]}" "$root/$dir" \
|| { echo "clone of $dir failed." >&2; exit 1; }
done
shadow_scan "$root"
echo "Repository ready at $root"
exit 0
}
```
- [ ] **Step 2: Verify the script still parses**
Run: `bash -n sbo-batch-test`
Expected: no output, exit 0.
- [ ] **Step 3: Commit**
```bash
git add sbo-batch-test
git commit -S -m "Add setup_repo (rm+rsync+clone+shadow, exit mode)"
```
---
### Task 7: `update_personal_trees` (inline, fail-soft)
**Files:**
- Modify: `sbo-batch-test` (new function after `setup_repo`)
- [ ] **Step 1: Write the implementation**
In `sbo-batch-test`, add after `setup_repo`:
```bash
# --update: per-run refresh. rsync the tree (--delete, personal excluded) to
# unshadow + refresh upstream, git-pull each personal subtree, re-shadow. Inline
# and fail-soft: a network hiccup warns but never kills the build session.
update_personal_trees() {
local root="${SBO_TREE_ROOTS[0]}"
[[ -n "$root" ]] || { echo "SBO_TREE_ROOTS[0] is empty; skipping --update." >&2; return; }
if [[ -n "$SBO_RSYNC_URL" ]]; then
echo "Refreshing tree (rsync, personal subtrees preserved)..."
rsync_tree "$root" || echo "WARN: rsync failed; continuing." >&2
else
echo "SBO_RSYNC_URL unset; skipping tree rsync (no unshadow)." >&2
fi
local dir d
for dir in "${!SBO_PERSONAL_TREES[@]}"; do
d="$root/$dir"
if [[ ! -d "$d/.git" ]]; then
echo "WARN: $d is not a git clone, skipping pull." >&2
continue
fi
echo "Pulling $dir..."
git -C "$d" pull || echo "WARN: git pull failed in $dir; continuing." >&2
done
shadow_scan "$root"
}
```
- [ ] **Step 2: Verify the script still parses**
Run: `bash -n sbo-batch-test`
Expected: no output, exit 0.
- [ ] **Step 3: Commit**
```bash
git add sbo-batch-test
git commit -S -m "Add update_personal_trees (rsync-unshadow + pull + shadow)"
```
---
### Task 8: Flags, usage, and main wiring
**Files:**
- Modify: `sbo-batch-test` globals (near line 75), `usage` (near 113), `parse_args` (139-160), `main` (894-901)
- [ ] **Step 1: Add flag globals**
In `sbo-batch-test`, next to `INIT_BASE=0` (line 75), add:
```bash
SETUP_REPO=0 # --setup-repo: rm+rsync tree, clone subtrees, shadow, then exit
DO_UPDATE=0 # --update: rsync-unshadow + pull subtrees + shadow before build
```
- [ ] **Step 2: Document both in usage**
In `usage()`, after the `--init-base` help block (around line 113-138), add two entries in the same style:
```
--setup-repo First-time build of the SBo tree: REMOVE SBO_TREE_ROOTS[0],
rsync it fresh from SBO_RSYNC_URL, clone the SBO_PERSONAL_TREES
subtrees in, shadow duplicated upstream packages, then exit.
Does not build. Runs as root.
--update Before building: rsync the tree (--delete, personal subtrees
preserved) to unshadow and refresh upstream, git-pull each
personal subtree, re-shadow duplicated upstream packages.
```
- [ ] **Step 3: Parse both flags**
In `parse_args`, add cases next to `--init-base` (line 147):
```bash
--setup-repo) SETUP_REPO=1; shift ;;
--update) DO_UPDATE=1; shift ;;
```
Then relax the "no target" guard (line 157) so `--setup-repo` needs no target:
```bash
if [[ -z "$TARGET_ARG" && $INIT_BASE -eq 0 && $SETUP_REPO -eq 0 ]]; then
echo "No target given." >&2; usage >&2; exit 2
fi
```
- [ ] **Step 4: Wire into main**
In `main` (lines 894-901), after `parse_args "$@"` and the `INIT_BASE` block, add `setup_repo` as an exit mode, and run `update_personal_trees` after `validate_env`. Result:
```bash
main() {
parse_args "$@"
init_color
if [[ $INIT_BASE -eq 1 ]]; then
init_base
fi
if [[ $SETUP_REPO -eq 1 ]]; then
setup_repo
fi
validate_env
[[ $DO_UPDATE -eq 1 ]] && update_personal_trees
...
```
Note: match the EXACT existing order around `init_color`/`init_base`/`validate_env` in the file; insert `setup_repo` right after the `init_base` block (both are pre-`validate_env` exit modes) and `update_personal_trees` right after `validate_env`. Read lines 894-910 first and splice in place.
- [ ] **Step 5: Verify parse + self-check**
Run: `bash -n sbo-batch-test && bash test-logic.sh`
Expected: parse clean; `ALL LOGIC CHECKS PASS`.
- [ ] **Step 6: Smoke the flags (no side effects)**
Run: `bash sbo-batch-test --help`
Expected: usage shows `--setup-repo` and `--update`.
Run: `SBO_BATCH_CONFIG=/nonexistent bash sbo-batch-test --setup-repo`
Expected: fails fast (not root, or missing config), does NOT rm anything.
- [ ] **Step 7: Commit**
```bash
git add sbo-batch-test
git commit -S -m "Wire --setup-repo and --update flags into main"
```
---
### Task 9: Docs (README + CLAUDE.md)
**Files:**
- Modify: `README.md`, `CLAUDE.md`
- [ ] **Step 1: README section**
Add a "Repository setup and refresh" section to `README.md` (near the other mode docs). Cover:
- `--setup-repo`: first-time, REMOVES and rsyncs the tree, clones personal
subtrees, shadows upstream dups, exits. Root. Interactive confirm.
- `--update`: before a build, rsyncs (--delete, personal preserved) to unshadow,
pulls personal subtrees, re-shadows. Fail-soft.
- The two config vars (`SBO_RSYNC_URL`, `SBO_PERSONAL_TREES`), noting the rsync
source is public slackbuilds.org, distinct from the NFS `LOCAL_MIRROR_15`.
- Shadowing: personal packages win; same-named upstream dup is rm -r'd. Dropping
a package from a personal repo self-heals on the next `--update` (rsync brings
the upstream copy back).
- [ ] **Step 2: CLAUDE.md updates**
In `CLAUDE.md`:
- Architecture list: add `rsync_excludes`, `rsync_tree`, `collect_shadows`,
`shadow_scan`, `setup_repo`, `update_personal_trees` with one-line roles.
- CONFIG-block description: add `SBO_RSYNC_URL` and `SBO_PERSONAL_TREES`.
- "What is verified vs not": note `collect_shadows` + `rsync_excludes` are
self-checked; rsync/clone/pull and the interactive shadow removal are NOT yet
run on hardware. Note the personal-exclude safety invariant.
- [ ] **Step 3: Verify no em dash**
Run: `grep -nP '\x{2014}' README.md CLAUDE.md config.example sbo-batch-test`
Expected: no output (no em dash anywhere).
- [ ] **Step 4: Final self-check + parse**
Run: `bash -n sbo-batch-test && bash test-logic.sh`
Expected: `ALL LOGIC CHECKS PASS`.
- [ ] **Step 5: Commit**
```bash
git add README.md CLAUDE.md
git commit -S -m "Document --setup-repo and --update"
```
---
## Notes for the implementer
- All commits GPG-signed (`-S`); the environment does not prompt. Verify with
`git log --format='%h %G? %s'` (want `G`).
- No em dash character anywhere (hard rule). Use commas/periods.
- Do NOT run any build (`bash *.SlackBuild`, `slackrepo`, `sbopkglint` on a
package). The user builds on their infra. You only edit + run `bash -n` and
`bash test-logic.sh`.
- Do NOT run `--setup-repo` / `--update` for real here: they rsync/rm/clone
against the VM tree and need root + network. Live-testing is the user's, on
buildsystem. The self-check covers the pure logic.
- After the plan lands, the script gets scp'd to buildsystem
(`/usr/local/bin/sbo-batch-test`) by the user.