aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-07-11-repo-setup-update.md
blob: 4392d888158ae2f7ef88c58f7a68688efca66a20 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
# 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=/<dir>/` 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 <root>, 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 <root>-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 <name>.SlackBuild; an upstream
# dup is a same-named depth-2 dir anywhere under <root> 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.