diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-20 20:30:57 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-20 20:30:57 +0200 |
| commit | 0602623806b7c0984544913d663804ed5f54e07f (patch) | |
| tree | fdd838b21d4dd0775f98760ca11d9e91e92d3745 /docs/superpowers/specs | |
| parent | cd9ca18cf12ab24e0d1532d01c0f621b06869545 (diff) | |
| download | sb-builder-0602623806b7c0984544913d663804ed5f54e07f.tar.gz sb-builder-0602623806b7c0984544913d663804ed5f54e07f.zip | |
README says what the GUI does today and what is planned, which defaults
are the author's setup, and how to set up the VM. The specs move to
docs/design/; the implementation plans are dropped (working files, now
kept out of git). AGENTS.md links slackrepo upstream instead of a local
clone path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers/specs')
6 files changed, 0 insertions, 1248 deletions
diff --git a/docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md b/docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md deleted file mode 100644 index 3ec3fc0..0000000 --- a/docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md +++ /dev/null @@ -1,335 +0,0 @@ -# sb-builder phase 1: core design - -Date: 2026-10-19 -Status: approved - -## Goal - -One Qt6 desktop GUI to drive the SlackBuild tooling on the build VM -(`buildsystem`): slackrepo, mkhint, overlay sync (sbpull, sbshadow), full repo -rebuild, and SBo orphan discovery. Long-running work runs as jobs on the VM -that survive the GUI disconnecting, and the GUI can reattach to them later. - -## Phases - -Each phase gets its own spec, plan and implementation cycle. - -1. **Core** (this spec): remote helper with the job model, GUI shell with - tabs, Dashboard, Jobs tab, VM control, config, notifications. Job kinds: - `sbpull`, `sbshadow`. -2. **Packages + slackrepo**: package browser over the large SBo-danix tree - (search and filter by name, category, overlay, maintainer, build status); - Build, Rebuild, Bulk update screens. -3. **Full repo rebuild**: `slackrepo_setup_SBo-danix.sh`, destructive, guarded. -4. **Hints**: mkhint browse, edit, update, create, `-F`, diff before save. - Requires non-interactive flags in mkhint (it has about 10 `read -p` - prompts and wraps slackrepo at `mkhint:1798`). Also: `--no-dl` must write - `DOWNLOAD="no"`, which slackrepo reads, instead of `NODOWNLOAD=yes`, - which it does not (keep reading old `NODOWNLOAD` lines for the NoDL column), - and existing hints on the VM get migrated: every `NODOWNLOAD=yes` line in - any hint slackrepo searches becomes `DOWNLOAD="no"` (an existing `DOWNLOAD` - line is replaced, since `DOWNLOAD="no"` overrides it anyway), backed up to - `.bak` first, idempotent. As of 2026-10-19 no hint carries the line. - Superseded by `2026-10-20-sb-builder-phase4-design.md`: the hint logic - moves into the helper instead of extending mkhint. -5. **Orphans**: SBo orphan discovery (`EMAIL="NOEMAIL"` in `.info`, the - `sbo-orphans` logic from slackbuilds-superrepo), including orphaned deps - in REQUIRES. Needs an SBo-stable tree, which is not on the VM today. - -Dashboard tiles become entry points as their screens land: Packages opens -the package browser (phase 2, select packages to build there), Built the -build screen, Last pull the sync actions, Recent jobs the Jobs tab, SBo -orphans the orphans screen (phase 5). - -Out of scope for all phases so far: overlay git operations (status, commit, -push in my-slackbuilds and friends). - -## Stack - -- GUI: Python 3 + PyQt6 (stock in Slackware -current, 6.11), including - `PyQt6.QtDBus` for notifications. No third-party packages. -- Helper: Python 3 stdlib only (VM runs 3.12, so `tomllib` is available). -- Config: TOML, read with `tomllib`. -- License: GPLv2-only. - -## Architecture - -``` -desktop buildsystem (root) -+----------------------+ ssh (ControlMaster) +---------------------------+ -| sb-builder (PyQt6) | ----------------------> | sbb-remote (py3 stdlib) | -| tabs: Dashboard, | sbb-remote <cmd> | stats / jobs / | -| Jobs (+later tabs) | <---------------------- | job-start / job-follow / | -| QProcess per call | JSON on stdout | job-kill, flock, setsid | -+----------------------+ | /var/lib/sb-builder/ | - | +---------------------------+ - | virsh (local libvirt) - v - domain "buildsystem" -``` - -### Repository layout - -``` -sb_builder/__main__.py entry point, config load -sb_builder/config.py GUI config: defaults, load, validate -sb_builder/remote.py the only code that runs ssh; also virsh -sb_builder/mainwindow.py tabs, menu, status bar, notifications -sb_builder/dashboard.py Dashboard tab -sb_builder/jobs.py Jobs tab -sb_builder/fmt.py text formatting shared by the tabs -sb-builder launcher (python3 -m sb_builder) -remote/sbb-remote VM helper, single file -config.example.toml GUI config example -remote/config.example.toml helper config example -install.sh local install; --remote deploys helper to VM -tests/test_remote.py -tests/test_gui.py -``` - -### Transport - -Every GUI call is a QProcess running: - -``` -ssh -o ControlMaster=auto -o ControlPath=<runtime dir>/sbb-%C -o ControlPersist=10m \ - <ssh_options...> <target> sbb-remote <cmd> [args...] -``` - -Calls are async. ssh exit 255 means the connection failed. Exit 127 means the -helper is not installed. - -### Trust boundary - -The helper runs only a fixed table of job kinds. The GUI sends a kind name -plus arguments; the helper validates each argument and builds argv itself. -No shell strings and no paths cross ssh. Validation rules: - -- unknown kind: rejected -- `sbpull` target: must be listed in the helper config `overlays` -- flags: only the ones the kind declares (`--yes`, `--no`) -- package names (phase 2 onward): `^[A-Za-z0-9._+-]+$` - -### Version check - -On connect the GUI runs `sbb-remote version`, which also returns the -configured `overlays` (the GUI builds its sbpull menu from them). A mismatch with the GUI's own -version shows a banner telling the user to run `./install.sh --remote`. - -## Helper: `sbb-remote` - -### State - -``` -<state_dir>/lock flock target, held for the whole job -<state_dir>/jobs/<id>/meta.json kind, args, argv, pid, start, end, exit, status -<state_dir>/jobs/<id>/log combined stdout+stderr, raw bytes -``` - -`<id>` is `YYYYMMDD-HHMMSS-<kind>`. `status` is one of `running`, `ok`, -`failed`, `killed`, `lost`, `busy`. - -Logs are kept forever (`# ponytail:` comment in code; add pruning once disk -usage is visible on the Dashboard). - -### Commands - -All print one JSON object on stdout, except `job-follow`. - -| command | behaviour | -|---|---| -| `version` | `{"version": "...", "overlays": [...]}` | -| `stats` | Dashboard data: load, uptime, disk free for repo and state dirs, package counts (total, personal, pentesting, shadowed = overlay packages also present in the pristine `current` branch, as sbshadow computes it), last sync per overlay (time of the newest commit touching `<overlay>/` in `repo_dir`), slackrepo summary from its sqlite DB (built, failed, never built). Any missing source yields `null` for that field, never an error | -| `jobs` | list of all jobs' meta. A job whose meta says `running` but whose pid is gone (`os.kill(pid, 0)` fails) is reported and rewritten as `lost` | -| `job-start <kind> [args]` | validate; spawn `sbb-remote _run <id>` with `start_new_session=True`, `stdin=/dev/null`, stdout/stderr to `/dev/null`; wait up to 3s for `_run` to write status; return `{"id": ...}`, or `{"busy": "<running id>"}` after deleting the refused job's dir | -| `job-follow <id> <offset>` | write log bytes from `offset` to stdout, keep following until the job is no longer `running`, then exit 0 | -| `job-kill <id>` | SIGTERM to the job's process group; `_run` records `killed` | -| `_run <id>` | internal, see below | - -### `_run` - -1. Take a non-blocking `fcntl.flock` on `<state_dir>/lock`. If held, write - status `busy` and exit. -2. Start the job argv with `stdin=/dev/null`, stdout and stderr to the log - file, in its own process group. Write `pid` and status `running`. -3. Wait. Write `end`, `exit` and final status (`ok` on 0, `killed` on - SIGTERM, else `failed`). - -Jobs never read input. A tool that prompts anyway gets EOF on `read` and, -under `set -e`, exits non-zero: the job fails, it never hangs and it never -answers yes by accident. Destructive steps are confirmed in the GUI. - -### Phase 1 job kinds - -| kind | argv built by helper | -|---|---| -| `sbpull <overlay> [--yes\|--no]` | `bash -c '. "$1" && sbpull "$2" "$3"' _ <sbpull_script> <overlay> <flag>` | -| `sbshadow [--yes\|--no]` | `sbshadow <flag>` | - -The flag is required in both kinds (a job has no stdin). - -### Helper config: `/etc/sb-builder/config.toml` - -```toml -state_dir = "/var/lib/sb-builder" -repo_dir = "/var/lib/sbopkg/SBo-danix" -upstream_branch = "current" -slackrepo_db = "/var/lib/slackrepo/SBo-danix/database_SBo-danix_x86_64.sqlite3" -sbpull_script = "/usr/local/share/slackrepo_setup/sbpull.bash" -overlays = ["personal", "pentesting"] -``` - -Missing file: these defaults. Unknown key or wrong type: every command -returns `{"error": "config: <key>: <reason>"}` and exits 1. - -### Errors - -Top-level handler catches any exception, prints `{"error": "<message>"}`, -exits 1. - -## Changes in slackrepo_setup - -Done in that repo, its own commit, redeployed to the VM. - -- `sbshadow --yes | --no`: no flag keeps the current interactive prompt. - `--yes` applies without asking. `--no` prints the plan and leaves the tree - unchanged (preview). `mkhint -F` runs in all cases, as it does today on - abort. -- `sbshadow` honours `SBO_REPO` to override the repo path, so it can be - tested against a throwaway repo. -- `sbpull <target> [--yes|--no]`: passes the flag to `sbshadow`, and returns - `sbshadow`'s exit status (today it returns `popd`'s, so a failure would - show as a successful job). - -GUI flow: `sbpull personal --no` pulls and shows the shadow plan in the log; -the Jobs tab offers "Apply shadowing", which starts `sbshadow --yes`. - -## GUI - -### Main window - -- QTabWidget. Dashboard is pinned. Other tabs are closable and - single-instance. -- Menu: `File > Reload config`, `File > Quit`, `Open > Jobs` (later phases - add entries). -- Status bar: connection state (ok, down, VM off, VM paused, VM running but - ssh unreachable, helper version mismatch), running job if any (click opens - it in Jobs), transient messages. - -### Dashboard - -Grid of QGroupBox tiles filled from `stats`, plus `jobs` for the Jobs tile: - -- VM: load, uptime, disk free -- Repo: total packages, personal, pentesting, shadowed -- slackrepo: built, failed, never built -- Sync: last pull per overlay -- Jobs: running job and last 10 (status, duration) -- Orphans: placeholder tile, filled in phase 5 - -Refreshes every `refresh.dashboard` seconds while visible, plus a Refresh -button. `null` fields show "n/a". When the connection is down the last -values stay, greyed, with "stale since HH:MM". - -### Jobs tab - -- Left: job list (status icon, kind and args, start, duration), refreshed - every `refresh.jobs` seconds. -- Right: log view (QPlainTextEdit, ANSI escapes stripped, `\r` rewrites the - current line), Kill button with confirm. -- Toolbar `Start`: one entry per overlay for `sbpull <overlay> --no`, plus - `sbshadow --no`, `sbshadow --yes` (confirm dialog). -- After a `--no` run whose log lists `shadow:` or `unshadow:` lines, an - "Apply shadowing" button starts `sbshadow --yes`. -- Selecting a job starts `job-follow` from offset 0. If the stream drops - while the job is still running, the next successful `jobs` poll restarts - it from the last received offset (the poll itself backs off while the VM - is down, see Error handling). -- `job-start` returning busy: dialog "busy: <id>" with [Show job]. - -### Notifications - -freedesktop `org.freedesktop.Notifications.Notify` via QtDBus. Events: -`finished`, `failed`, `killed`, `lost`, and `vm-up` (VM started and ssh -answering). Clicking a notification (`ActionInvoked`) raises the window and -opens the job. The GUI tracks the job states it has seen, so each job end -notifies once. - -### VM control - -When ssh fails and `vm.domain` is set, the GUI runs -`virsh -c <libvirt_uri> domstate <domain>` locally: - -| domstate | behaviour | -|---|---| -| `shut off` | status "VM off", [Start VM] (automatic when `vm.autostart`): `virsh start`, then poll ssh with backoff, notify `vm-up` | -| `paused` | status "VM paused", [Resume]: `virsh resume` | -| `running` | status "VM running, ssh unreachable", keep retrying | -| virsh missing or error | plain "down" | - -No shutdown control. - -### GUI config: `$XDG_CONFIG_HOME/sb-builder/config.toml` - -```toml -[remote] -target = "buildsystem" # any ssh destination -ssh_options = [] # extra ssh args; ControlMaster options always added - -[refresh] -dashboard = 30 -jobs = 5 - -[notify] -enabled = true -events = ["finished", "failed", "killed", "lost", "vm-up"] - -[vm] -libvirt_uri = "qemu:///system" -domain = "buildsystem" # empty disables VM control -autostart = false -``` - -Missing file: these defaults. Invalid key or type: dialog naming the key, -then defaults are used. Read at startup and on `File > Reload config`. No -settings dialog yet. - -## Error handling summary - -| case | behaviour | -|---|---| -| ssh down | status bar, VM check, stale tiles; the `jobs` poll interval doubles from `refresh.jobs` up to 30s, and resets once ssh answers | -| helper missing or version mismatch | banner with `./install.sh --remote` | -| busy | dialog with [Show job] | -| follow drop | reconnect from offset, job unaffected | -| helper exception | `{"error"}` shown in status bar | -| `_run` died, VM rebooted mid-job | `lost`, red in list, notification | -| config invalid | dialog naming key, defaults used | - -## Testing - -stdlib `unittest`, no frameworks. - -- `tests/test_remote.py`: imports `remote/sbb-remote` as a module with a - temp `state_dir`. Covers argument validation (unknown kind, overlay not in - config, unknown flag), lock busy, `lost` detection, `job-follow` from an - offset, and a full `_run` of a fake kind patched into the kind table - (ok, failed, killed). -- `tests/test_gui.py`: config load and validation, `remote.parse`, GUI and - helper versions equal, both example configs equal the defaults. Smoke tests - under `QT_QPA_PLATFORM=offscreen` with a fake Remote: tabs, Dashboard from - a canned `stats` dict with `null` fields, job-end notification sent once, - VM-off offer, carriage-return handling in the log view. -- slackrepo_setup: sbshadow `--yes` and `--no` verified in a throwaway git - repo, the same way the current behaviour was tested. -- First run against the real VM: manual, with the user. - -## Install - -- `install.sh`: symlinks the `sb-builder` launcher into `~/bin` (`BINDIR` - overrides), so repo edits are live; copies `config.example.toml` to the - config path only if no config exists. -- `install.sh --remote`: scp `remote/sbb-remote` to `<target>:/usr/local/bin/` - (`SBB_TARGET`, default `buildsystem`), - create `/etc/sb-builder/config.toml` from the example only if missing. - Asks before deploying. A SlackBuild comes later. diff --git a/docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md b/docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md deleted file mode 100644 index 21a041f..0000000 --- a/docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md +++ /dev/null @@ -1,295 +0,0 @@ -# sb-builder phase 2: packages and slackrepo builds - -Date: 2026-10-19 -Status: approved -Builds on: `2026-10-19-sb-builder-phase1-design.md` (helper, job model, GUI shell) - -## Goal - -Browse and search the ~10k packages of the SBo-danix tree, select packages -and run slackrepo `build`, `rebuild`, `update` and `remove` on them, plus -`update` of everything, as jobs. Every run is previewed first. Publishing -(the rsync push of the package repository) can be held back per run and -done later with a separate Publish action. Package signing works from the -GUI although jobs have no terminal. - -## Facts this design rests on - -Verified on the VM and in the slackrepo source (`~/Programming/GIT/GITHUB/slackrepo`): - -- slackrepo usage: `slackrepo {build|rebuild|update|remove} [--preview|--dry-run] [item...]`, - `--repo=SBo-danix`. Items may be bare package names. -- `--preview` on build/update does **not** set `OPT_DRY_RUN` (only revert and - remove do, `libexec/functions.d/cmdfunctions.sh:231,398`). Finish hooks that - test only `OPT_DRY_RUN` still run during a preview: today `gotify_hook` - sends a message and `manage_packages_hook` runs without `--dry-run`. -- The live config `/etc/slackrepo/slackrepo_SBo-danix.conf` defines start hook - `danix_gitfetch_hook` (fetch, rebase `danix-current` onto upstream; runs on - every invocation, previews included) and finish hooks - `manage_packages_hook bell_hook genrepos_hook gen_web_hook dep_template_hook rsync_push_hook recap_hook gotify_hook`. - It contains a Gotify key and site hostnames. Stale copies exist in - `repo-html-structure/` and in the upstream slackrepo clone. -- `genrepos_hook` runs `gen_repos_files.sh`, which signs with gpg. - `/root/.genreprc` sets `USE_GPGAGENT=1` and `REPOSOWNERGPG=<fingerprint>`. - The agent uses `pinentry-curses` (needs a tty) with - `default-cache-ttl 14400`, `max-cache-ttl 21600`. With the key uncached - and no tty, `gen_repos_files.sh` aborts ("GPG test failed"). -- slackrepo DB (`database_SBo-danix_x86_64.sqlite3`): `revisions` row with - `dep='/'` holds the built `version` and `built` time per itemid - (`category/pkg`); `buildresults` holds the last `result` per itemid - (`ok`, `failed`, `bad`, `aborted`, `removed`). 213 of 799 `buildresults` - rows have legacy keys without a `/` and match no package. -- Hints: 623 in `/etc/slackrepo/SBo-danix/hintfiles/`; the default hint dir - (`default_hintfiles/15.0`) is empty. Real usage: 594 `DELREQUIRES`, 13 each - of `VERSION`, `DOWNLOAD`, `MD5SUM`, `ARCH`, 1 `OPTIONS`. -- Hint grammar (`libexec/functions.d/parsefunctions.sh:405-640`, - `man/man5/slackrepo.hint.5`): slackrepo searches `<SBREPO>/<cat>/<pkg>/`, - `HINTDIR/`, `HINTDIR/<cat>/<pkg>/`, `DEFAULT_HINTDIR/`, - `DEFAULT_HINTDIR/<cat>/<pkg>/` for `<pkg>.hint`, first match wins, and - sources it. Recognised keys: `SKIP VERSION OPTIONS GROUPADD USERADD - NOWARNING NOWARNINGS ADDREQUIRES DELREQUIRES BUILDTIME CONFLICTS INSTALL - NUMJOBS ANSWER CLEANUP PRAGMA SPECIAL ARCH DOWNLOAD MD5SUM SHA256SUM`, plus - `DOWNLOAD_<arch>`, `MD5SUM_<arch>`, `SHA256SUM_<arch>` resolved through - `ARCH`. The man page says `ANSWERS`; the code reads `ANSWER`. -- mkhint `--no-dl` writes `NODOWNLOAD=yes`, which slackrepo does not read - (its switch is `DOWNLOAD="no"`). No current hint has it. Phase 4 changes - mkhint to write `DOWNLOAD="no"`. - -## Part 1: VM side, in the slackrepo_setup repo - -slackrepo_setup becomes the source of truth for the slackrepo config. - -- `slackrepo_SBo-danix.conf`: imported from the VM. Site values move out to - `/etc/slackrepo/SBo-danix.local` (root-only, mode 600, not in git), which the - config sources: rsync target, rsync password and excludes files, Gotify URL - and key. The repo ships `SBo-danix.local.example` with placeholders. -- `sb-publish`: new script, the rsync push of `PKGREPO` that - `rsync_push_hook` does today. Reads its settings from the `.local` file. - `rsync_push_hook` calls it. -- Gates in the config: - - `rsync_push_hook`: return 0 when `SBB_PUBLISH=n`. - - `gotify_hook`: return 0 when `OPT_PREVIEW=y` (as well as `OPT_DRY_RUN=y`). - - `manage_packages_hook`: pass `--dry-run` when `OPT_PREVIEW=y` too. -- `gpg-agent.conf` for root, managed here: today's content plus - `allow-preset-passphrase`. Deploy runs `gpgconf --reload gpg-agent`. -- Terminal runs without `SBB_PUBLISH` behave as today, except that previews - no longer notify Gotify or touch packages. -- The stale config copy in `repo-html-structure` (untracked, may still hold - the Gotify key) is deleted after asking the user. -- Deploy (asks first): config to `/etc/slackrepo/`, `.local` created from the - current live values only if missing, `sb-publish` to `/usr/local/bin`, - `gpg-agent.conf` to `/root/.gnupg/`. - -## Part 2: helper (`sbb-remote` 0.2.0) - -### New config keys - -```toml -slackrepo_repo = "SBo-danix" -hintdir = "/etc/slackrepo/SBo-danix/hintfiles" -default_hintdir = "" # optional, searched after hintdir -genreprc = "/root/.genreprc" -``` - -### `inventory` - -One JSON object `{"columns": [...], "rows": [[...], ...]}`, a row per -package directory `<cat>/<pkg>/<pkg>.SlackBuild` in `repo_dir`: - -| column | source | -|---|---| -| `path` | `cat/pkg` | -| `version` | hint `VERSION` if the hint has one, else `.info` `VERSION` | -| `info_version` | `.info` `VERSION` | -| `hint_version` | hint `VERSION` or `null` | -| `built_version`, `built_time` | `revisions`, `dep='/'`, by path; `null` if none | -| `result` | `buildresults` by path; `null` if none | -| `maintainer`, `requires` | `.info` `MAINTAINER`, `REQUIRES` (list) | -| `summary` | `slack-desc` line 1, text inside the parentheses | -| `hint_path` | the hint found by slackrepo's search order (repo package dir, `hintdir`, `hintdir/<cat>/<pkg>`, `default_hintdir`, `default_hintdir/<cat>/<pkg>`), or `null` | -| `hints` | `{key: raw value}` of every assignment in that hint, or `null` | - -`.info` and hint files are parsed, never sourced: continuation lines joined, -then `shlex.split(line, comments=True)`, keeping `NAME=value` tokens. A -value containing `$` is kept as written and flagged `"shell": true` for the -GUI, since it would be expanded by the shell. A missing DB yields `null` build -fields, not an error. - -### Job kinds - -| kind | argv | env | -|---|---|---| -| `slackrepo <action> [--preview] [--no-publish] [pkg...]` | `slackrepo --repo=<slackrepo_repo> <action> [--preview] pkg...` | `SBB_PUBLISH=n` with `--no-publish` | -| `publish` | `sb-publish` | | - -- `action` is one of `build`, `rebuild`, `update`, `remove`. -- Package names match `^[A-Za-z0-9._+-]+$` and must name a package in the - tree. Duplicates are dropped. -- `build`, `rebuild`, `remove` need at least one package; `update` with none - means all. -- Kinds now return `(argv, env)`; `_run` merges `env` into the environment. - Existing kinds return an empty env. `meta.json` records `env`. -- While the signing key is locked, `job-start` refuses a non-preview - `slackrepo` job with `{"error": "signing key locked"}`. Previews and - `publish` (which does not sign) are not affected. - -### Signing key - -- Key: `REPOSOWNERGPG` read from `genreprc` with a regex over - `REPOSOWNERGPG="..."` (never sourced). Keygrips from - `gpg --with-colons --with-keygrip --list-secret-keys <key>` (`grp` lines). -- `gpg-status`: `gpg-connect-agent "keyinfo <grip>" /bye` per keygrip; the key - is unlocked when every keygrip reports cached. Returns - `{"unlocked": bool, "key": "<fingerprint>"}`; `{"unlocked": null}` when no - key is configured. -- `gpg-unlock`: reads the passphrase from stdin, runs - `gpg-preset-passphrase --preset <grip>` per keygrip with the passphrase on - its stdin, then verifies by signing a temp file with - `gpg --batch --pinentry-mode error -u <key> -bas`. On failure it runs - `gpg-preset-passphrase --forget` per keygrip and returns - `{"error": "bad passphrase"}`. The passphrase is never put on a command - line, in the environment, in a log or on disk. -- Found 2026-10-20: the preset passphrase is lost when gpg-agent exits, and - it exits when root's last login session ends. elogind then removes - `/run/user/0` (root does not linger), where the agent's sockets live; the - agent notices its socket is gone and terminates. The GUI's ssh - ControlMaster keeps a session open while sb-builder runs, so the key stays - unlocked as long as the GUI does, and locks about 10 minutes after it - quits (ControlPersist). Accepted as is; `loginctl enable-linger root` - would keep the agent across sessions. The GUI checks state before every - real run either way. - -### Version - -`VERSION = "0.2.0"` in both helper and GUI; the existing banner asks for -`./install.sh --remote` until the VM is updated. - -## Part 3: GUI - -### Packages tab (`sb_builder/packages.py`, `Open > Packages`) - -Layout B from brainstorming: query bar, chip rows, table, detail pane, action -bar. - -- Model: `QAbstractTableModel` over the cached inventory, with a - `QSortFilterProxyModel` subclass doing the filtering. Columns: check, name, - category, version, built, status, maintainer. Sortable. -- Status, derived in the GUI: - - | status | rule | - |---|---| - | `never` | no `built_version` and no `result` | - | `failed` | `result` in `failed`, `bad`, `aborted` | - | `outdated` | `built_version` != `version` | - | `ok` | otherwise | - - Shown as a colored glyph plus the word (status colors from the Dashboard). - `# ponytail:` outdated compares versions only; slackrepo also rebuilds on - SlackBuild or dependency changes, which its preview shows. -- Query bar: whitespace-separated terms, all must match, case-insensitive - substring. Plain term: name or summary. `cat:`, `maint:`, `req:`, - `status:` match that field. -- Chips: overlay group (`personal`, `pentesting`, `upstream` = any other - category) and status group (`ok`, `failed`, `never`, `outdated`). OR within - a group, AND across groups and with the query; an empty group does not - filter. -- Detail pane for the current row: path, summary, `.info`, hint and built - versions, built time, last result, maintainer, and the hint view below. - -### Hint view (`sb_builder/hints.py`, in the detail pane) - -Read-only and structured; never the raw file. `hints.interpret(hints, info)` -returns sections, the detail pane renders only those that apply: - -| section | keys | shown as | -|---|---|---| -| Skipped (banner) | `SKIP` | `no` = no banner; `yes` = always skipped; other text = skipped, the text as reason | -| Version override | `VERSION`, `ARCH`, `DOWNLOAD[_arch]`, `MD5SUM[_arch]`, `SHA256SUM[_arch]` | `.info` version -> hint version; URLs after `ARCH` resolution (`DOWNLOAD_<ARCH>` wins when set); "checksums not checked" when `MD5SUM="no"` or `VERSION` set without `MD5SUM` (slackrepo forces `no`); "no download, source must be in the cache" for `DOWNLOAD="no"` and for `NODOWNLOAD=yes` (shown identically, by user decision) | -| Dependencies | `.info` `REQUIRES`, `DELREQUIRES`, `ADDREQUIRES`, `BUILDTIME`, `PRAGMA python3`, `CONFLICTS` | one chip row computed as slackrepo does: `.info` deps minus `DELREQUIRES` and `%README%` (struck through), `python3` added by the pragma, `ADDREQUIRES` added (+), `BUILDTIME` added and marked build-time only (not in package metadata); `CONFLICTS` listed apart. With no `.info` `REQUIRES`, `ADDREQUIRES` alone defines the deps | -| Build settings | `OPTIONS`, `NUMJOBS`, `INSTALL`, `ANSWER`, `NOWARNING`/`NOWARNINGS` | rows; `OPTIONS` split into `OPT=value` pairs; `INSTALL` as yes/no by its first letter (`Y`/`1`, `N`/`n`/`0`); `ANSWER` with `\n` shown as a return glyph; `NOWARNING` labelled as a regex | -| Pragmas | `PRAGMA`, `SPECIAL` | one row per pragma with its meaning from `slackrepo.hint(5)`; `x86arch=<arch>` and `unset_<VAR>` parsed; unknown pragmas shown as unknown | -| Host changes | `GROUPADD`, `USERADD` | table rows: gid, name; uid, name, group, home, shell, extra args, with slackrepo's defaults filled in (name = package, group = user, home `/dev/null`, shell `/bin/false`) | -| On uninstall | `CLEANUP` | the command | -| Ignored | any other key | "not read by slackrepo" (except `NODOWNLOAD`, see above) | - -Values flagged `shell` show as written, marked "expanded by the shell". The -footer shows `hint_path`. -- Selection: check column, "Select visible", "Clear", "N selected" label. - Actions apply to the checked packages, or to the current row when none is - checked. -- Action bar: Build, Update, Rebuild, Remove, Update all, and Publish - (confirm dialog, starts the `publish` job). -- Build/Update/Rebuild/Remove/Update all start the preview job - (`slackrepo <action> --preview ...`) and open it in the Jobs tab. -- Inventory: fetched when the tab opens, on its Refresh button, and after any - `slackrepo` or `publish` job ends. While loading, the old rows stay and the - status line says "loading...". - -### Jobs tab additions - -- A finished `ok` preview of kind `slackrepo` shows [Run it] and a ☑ publish - checkbox (initial state from config `builds.publish`). Run it starts the - same action and packages without `--preview`, adding `--no-publish` when the - box is unticked. -- Before starting any non-preview `slackrepo` job (Run it, or anywhere else) - the GUI calls `gpg-status`; when locked it asks for the passphrase - (`QInputDialog` in password mode), calls `gpg-unlock` with the passphrase on - stdin, repeats on `bad passphrase`, then starts the job. Cancel aborts. -- `remote.call` gains an optional `stdin` bytes argument, written to the - QProcess and closed. - -### Main window and Dashboard - -- Status bar: signing key state, 🔒 locked / 🔓 unlocked, refreshed with the - stats poll; hidden when the helper reports no key. -- Dashboard: clicking the Packages tile opens the Packages tab, clicking the - Recent jobs card opens the Jobs tab. -- `Open` menu: Packages, Jobs. - -### GUI config - -```toml -[builds] -publish = true # initial state of the publish checkbox -``` - -## Error handling - -| case | behaviour | -|---|---| -| inventory slow (cold disk ~7s) | async; old rows stay, "loading..." | -| inventory call fails | status bar message, old rows stay | -| preview fails | no [Run it]; the log says why | -| key locked | password dialog; `bad passphrase` asks again; Cancel aborts | -| `job-start` refuses (locked) | same dialog, then retry once | -| busy | existing busy dialog | -| `genrepos` fails anyway | job `failed`, notification, slackrepo's message in the log | -| unknown package | helper rejects by name | - -## Testing - -stdlib `unittest`, headless, no VM. - -- Helper: `inventory` on a fake tree, hint dirs and sqlite DB (hint version - wins, hint search order precedence, shlex parsing with quotes, comments and - continuation lines, `$` values flagged, legacy DB keys ignored, missing DB - gives nulls); - `slackrepo` kind (unknown package, `build` without packages, `update` - without packages, duplicate names, `--no-publish` sets - `SBB_PUBLISH=n` in env and in the child process); locked key refuses a real - run, not a preview (gpg check patched); `gpg-unlock` feeds stdin to a fake - `gpg-preset-passphrase` on `PATH` and forgets on failed verification. -- GUI: `hints.interpret` table-driven per key and derived rule (ARCH - resolution, forced checksum skip, dependency computation incl. python3 - pragma and `ADDREQUIRES`-only, USERADD/GROUPADD defaults, SKIP forms, - INSTALL letters, `NODOWNLOAD` shown as `DOWNLOAD="no"`, ignored keys, - shell-flagged values); query parser and filter (prefixes, chips, OR/AND); status - derivation; action target is checked rows else current row; [Run it] after - an ok preview, with `--no-publish` when unticked; locked key opens the - dialog before the real job (dialog patched); Dashboard tile clicks open - tabs. -- slackrepo_setup: `bash -n` on config and `sb-publish`; source the config - with stub `rsync`, `curl` and `manage-packages.sh` on `PATH` and check the - gates with and without `SBB_PUBLISH=n` and `OPT_PREVIEW=y`. -- First real run with the user: unlock, preview an update of one package, - run it without publish, then Publish. diff --git a/docs/superpowers/specs/2026-10-20-sb-builder-bundled-deps-design.md b/docs/superpowers/specs/2026-10-20-sb-builder-bundled-deps-design.md deleted file mode 100644 index de6ad1d..0000000 --- a/docs/superpowers/specs/2026-10-20-sb-builder-bundled-deps-design.md +++ /dev/null @@ -1,115 +0,0 @@ -# sb-builder: bundled deps (phase 4b, part 2) - -Date: 2026-10-20 -Status: approved -Builds on: `2026-10-20-sb-builder-phase4-design.md`, `2026-10-20-sb-builder-phantom-deps-design.md` - -## Goal - -`hint-update` handles packages with several downloads, so mkhint's bundled-dep -code (url mode) is no longer needed and part 3 can remove mkhint from the VM. - -## Facts - -- 64 packages are tracked in `nvchecker.toml`; 4 have more than one download - (686 in the whole tree): - - neovim, 13 extra: upstream lists them in `cmake.deps/deps.txt` - (`NAME_URL <url>` lines), at a path fixed per tag. - - openvino, 9 extra: git submodules pinned by commit. Lives in the personal - overlay, its `.info` is edited there; not hint-managed. - - feroxbuster, 1 extra: a self-hosted vendor tarball carrying the main - version (`feroxbuster-<ver>-vendor.tar.gz`). - - exploitdb, 1 extra: `exploitdb-bin-sploits`, its own version, unchanged - since 2022. -- `apply_version` swaps the version through the whole hint text but - downloads only the first URL of each `DOWNLOAD*` for its md5; other URLs - keep their old md5. A feroxbuster bump today writes the new vendor URL - with the old md5. -- mkhint's url mode (`/usr/local/bin/mkhint`, `reconcile_bundle_deps_url` and - helpers, about 200 lines of bash) fetches the manifest at the new version, - takes the `_URL` lines, matches each extra hint URL to one, compares - versions and swaps the changed ones, downloading them for the md5. - -## Scope - -In: md5 for every changed URL (all packages), manifest reconcile (packages -in `[manifests]`). - -Out: git submodule mode (openvino, not hint-managed), separately versioned -extras with no manifest (exploitdb), a GUI for the manifest map, adding deps -the manifest has and the hint lacks (logged only), part 3 (removing mkhint, -nvchecker and their config from the VM). - -## Helper (`sbb-remote` 0.8.0) - -### Config - -`[manifests]` table, package -> manifest URL template, `{VERSION}` replaced -by the new version. The default holds neovim's, so the VM needs no new line. -Replaces `manifest_packages`, and with it the refusal of manifest packages by -`hint-update` and `hint-new`. `remote/config.example.toml` shows the default: - - [manifests] - neovim = "https://raw.githubusercontent.com/neovim/neovim/v{VERSION}/cmake.deps/deps.txt" - -`manifest_packages` becomes an unknown key: the deploy drops its line from -the VM's `/etc/sb-builder/config.toml`, or the helper refuses to start. - -### `apply_version`, every package - -After the version swap, for each `DOWNLOAD*`/`MD5SUM*` pair, download every -URL that differs from the one at the same position before the swap, plus the -first URL as today, and set its md5. Unchanged continuation URLs keep their -md5. `SKIP_URLS` (`UNSUPPORTED`, `UNTESTED`) and `--no-dl` behave as today. - -### `apply_version`, packages in `[manifests]` - -Runs after the version swap and before the downloads, only when a new -version is given and `--no-dl` is not: - -1. Fetch the manifest at the new version. -2. Extract every `https?://` URL from it (regex, any file format). -3. For each extra URL (position 1 and later) of `DOWNLOAD`, find its match: - first by GitHub `owner/repo`, lowercased, skipping the shared blob repo - `neovim/deps`; then by stem, the basename without archive extension and - without a trailing `-<version>` (`[vV]?` digit, or 7+ hex). -4. Version of a URL: basename without archive extension; on GitHub, strip a - leading `<repo>-` or `<repo>_` (case-insensitive) and the rest is the - version; elsewhere, the trailing `-<version>` or `-<sha>`. Then drop a - leading `v` and turn `-` into `_`. The first URL's manifest counterpart is - never reconciled. -5. A match whose version differs replaces the hint URL; the download step - above then gives it its md5. - -Log lines: `<pkg>: <dep> <old> -> <new>` per changed dep, `<pkg>: <dep> no -manifest match, left as is`, `<pkg>: manifest has N deps not in the hint`. - -### Errors - -A manifest fetch failure, or a download failure, fails that package: its -hint and `.bak` stay untouched, `<pkg>: failed: <why>` is logged, the job -moves on, as for the main download today. An empty manifest (no URLs) is a -failure too: a moved file must not pass as "all current". - -## GUI - -None. The hint diff dialog already shows the changed `DOWNLOAD`/`MD5SUM` -lines. - -## Testing - -`test_remote.py`, no network (the existing `Served` localhost http server): - -- URL version and match against neovim's real URL shapes: github archive - `v<ver>/<name>-<ver>`, LuaJIT by commit, luv release asset, lpeg off - GitHub, mixed-case owner (JuliaStrings). -- manifest reconcile: one dep changed, one unchanged, one unmatched; the - changed one gets the new URL and md5, the rest keep theirs. -- a continuation URL carrying the main version gets a new md5 without a - manifest (feroxbuster). -- manifest fetch failure leaves the hint untouched. - -## Version - -0.8.0 in `remote/sbb-remote` and `sb_builder/__init__.py`; deploy with -`./install.sh --remote` after asking. diff --git a/docs/superpowers/specs/2026-10-20-sb-builder-phantom-deps-design.md b/docs/superpowers/specs/2026-10-20-sb-builder-phantom-deps-design.md deleted file mode 100644 index 46bc41c..0000000 --- a/docs/superpowers/specs/2026-10-20-sb-builder-phantom-deps-design.md +++ /dev/null @@ -1,74 +0,0 @@ -# sb-builder: phantom deps (phase 4b, part 1) - -Date: 2026-10-20 -Status: approved -Builds on: `2026-10-20-sb-builder-phase4-design.md`, `2026-10-20-sb-builder-ui-design.md` - -## Goal - -Manage the -current phantom deps (deps SBo packages still require but -Slackware -current ships) from sb-builder: the list, suggestions from the -tree, and the `DELREQUIRES` sweep that mkhint's `-F` does today. Found when --current added zenity: 8 SBo packages require it, nothing in the GUI could -mark it. - -## Facts - -- When a package enters the official tree, SBo removes it. A `REQUIRES` - name that matches no package of the repo is then left behind. On - 2026-10-20 the tree has 140 such names, zenity among them; the 17 on - mkhint's list (`/root/.config/mkhint/phantom-deps`) are all among them. -- sbshadow ends with `mkhint -F` (skipped on `--no`). mkhint's `-F` merges - the phantom deps found in each `.info` `REQUIRES` into the hint's - `DELREQUIRES`, `.bak` first, a minimal hint when there is none; it never - removes entries. -- openvino moved into the personal overlay; only neovim still needs mkhint's - bundled-dep logic. - -## Helper (`sbb-remote` 0.7.0) - -- Config: `phantom_deps_file`, default `/etc/sb-builder/phantom-deps`: one - name per line, `#` comments, mkhint's format. Missing file = empty list. - `manifest_packages` default becomes `["neovim"]`. -- `phantom-list`: `{"deps": [...], "suggestions": [{"name", "needed_by": - [...]}]}`. Suggestions: every `REQUIRES` name (without `%README%`) that - matches no package of the repo and is not on the list, sorted, with the - packages requiring it. -- `phantom-add <name>...`, `phantom-remove <name>...`: names must match - `PKGNAME`, need not exist in the repo. Rewrites the file atomically, - comments kept, one name per line, no duplicates. Returns the new list. -- Job kind `fix-current`: for every package whose `.info` `REQUIRES` names a - listed dep, merge those deps into `DELREQUIRES` of `<hintdir>/<pkg>.hint` - (union, first seen order; `.bak` only when it changes; a new hint has just - that line and no `.bak`). Unmanaged hint paths fail that package. Log one - line per changed hint, then `SBB-RESULT`. Add only, like mkhint. -- `fix-current-now`: the same sweep run inline, its log on stdout, for - sbshadow. - -## sbshadow (slackrepo_setup) - -The end of a non-preview run calls `sbb-remote fix-current-now` in place of -`mkhint -F`; without sbb-remote it says so and skips, as it does for mkhint -today. - -## GUI - -Hints ribbon: **Phantom deps…** (between Delete hint and the end). A -non-modal dialog: - -- the list, one name per row, with **Remove** for the selected rows; -- a name field with **Add**; -- **Suggestions**: tickable rows `name needed by a, b, c`, **Add ticked**; -- **Apply**: starts `fix-current` (the list itself is saved by each Add or - Remove at once); the usual diff dialog follows, with Revert per package. - -## Deploy (asks first) - -1. Helper 0.7.0. -2. `/etc/sb-builder/phantom-deps` from mkhint's file (17 entries). -3. sbshadow. - -## Out - -neovim bundled deps (phase 4b, part 2), removing mkhint and nvchecker from -the VM (part 3), removing `DELREQUIRES` entries when a dep leaves the list. diff --git a/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md b/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md deleted file mode 100644 index b746044..0000000 --- a/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md +++ /dev/null @@ -1,266 +0,0 @@ -# sb-builder phase 4: hints - -Date: 2026-10-20 -Status: approved -Builds on: `2026-10-19-sb-builder-phase1-design.md` (helper, job model, GUI shell), -`2026-10-19-sb-builder-phase2-design.md` (inventory, hint view, slackrepo jobs) - -## Goal - -Manage slackrepo hint files from sb-builder: check upstream versions, update -version hints, create and delete hints, and see a diff of every change -with a way to revert it. - -Direction set by the user: the repo is run from the desktop, the VM shell is -for upgrading Slackware only. The VM-side tools were built for a terminal; a -tool may lose its in-VM usability if that makes sb-builder simpler. So the -hint logic of mkhint moves into `sbb-remote` (Python, JSON answers) and the -upstream check moves to the desktop. mkhint is not extended. - -## Facts this design rests on - -Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`, -1.4.0, 2291 lines) and in slackrepo (`../GITHUB/slackrepo`): - -- The VM has 607 hints, all in `hintdir`, none in subdirectories and none in - the SBo tree. 594 carry only `DELREQUIRES`, 13 carry - `VERSION` + `ARCH` + `DOWNLOAD` + `MD5SUM`, one carries `OPTIONS`. No hint - carries `NODOWNLOAD`. Pinned hints carry `DOWNLOAD_x86_64`/`MD5SUM_x86_64` - lines, mostly empty (`claude-code`: `DOWNLOAD="UNSUPPORTED"` with the URL in - `DOWNLOAD_x86_64`), and `ARCH="x86_64"`. -- slackrepo reads `DOWNLOAD="no"` as "no download" and ignores `NODOWNLOAD`. - `DOWNLOAD_$ARCH` replaces `DOWNLOAD` when set - (`libexec/functions.d/parsefunctions.sh:457`), so `DOWNLOAD="no"` next to a - `DOWNLOAD_x86_64` line has no effect. -- mkhint's version update: back up to `.bak`, replace the old version by the - new one everywhere in the file, also the `_` -> `-` form when either version - contains `_`, also the dotless form of a dotted version inside URLs as a - whole number (`_swap_dotless_url_version`), then for `DOWNLOAD`/`MD5SUM` and - `DOWNLOAD_x86_64`/`MD5SUM_x86_64`: download the first URL and recompute its - md5, keep the md5 of continuation URLs unless the user typed a new URL. - `UNSUPPORTED`/`UNTESTED` values are skipped. -- mkhint's new hint from `.info`: copy the `.info`, drop `PRGNAM`, `HOMEPAGE`, - `MAINTAINER`, `EMAIL`, comment out `REQUIRES`, set `ARCH="x86_64"`, append - `DELREQUIRES` for phantom deps in `REQUIRES`, then apply a version as above - when one is given. -- mkhint's `-F`: for every `.info` in the repo whose `REQUIRES` contains a - phantom dep, merge those deps into the hint's `DELREQUIRES` (union, first - seen order, `.bak` only when something changes, minimal hint created when - absent). It edits only the `DELREQUIRES` line. sbshadow - (slackrepo_setup) runs it after every sync: that is where the 594 - DELREQUIRES-only hints come from, and why they exist (slackrepo must not - build deps -current does not need). Until now sbshadow ran it on `--no` - previews too. -- The VM's nvchecker config has 64 stanzas named after the packages, plus a - `[__config__]` with oldver, newver and keyfile. nvchecker v2.22 is - installed on the desktop, with a GitHub token in - `~/.config/nvchecker/keys.toml`. With `--logger json` it prints one JSON - object per line on stdout: `{"name", "version", "event": "updated"}` per - result, `{"name", "error", "level": "error"}` per failure; it exits 0 when - entries fail and needs no oldver or newver to run. -- A DELREQUIRES-only hint has no `VERSION`; mkhint's update cannot bump it. - Updating one pins it from the `.info` first; the next `mkhint -F` keeps the - pin. A package with a phantom dep always has such a hint, so `hint-new` - never sees one. -- slackrepo `update <item>` builds an item that exists, built or not - (`update_command` calls `build_command`). -- The phantom deps list on the VM (`/root/.config/mkhint/phantom-deps`) has - 18 entries. -- Two packages use bundled-dep manifests: neovim (url mode) and openvino - (sha mode, GitHub submodules). That code is about 470 lines of mkhint. - -## Scope - -In: upstream check, version update, new hint, delete hint, -`--no-dl` as `DOWNLOAD="no"`, diff and revert. - -Out, for good (the Hints screen covers them): mkhint `--list`, `--versions`, -`--review`, `--strip-version`, `--info`, `--clean`. - -Out, later (phase 4b, which then removes mkhint and nvchecker from the VM): -bundled-dep reconcile for neovim and openvino, and a helper `fix-current` -that sbshadow calls in place of `mkhint -F`, with the phantom deps list -moved into the helper config as its single source. Also out: generating nvchecker stanzas -for new packages (add them to `nvchecker.toml` by hand), free text hint -editing, migration of `NODOWNLOAD=yes` lines (none exist). - -## Part 1: upstream check, on the desktop - -- `nvchecker.toml` in the repo root: the VM's 64 stanzas, without - `[__config__]`. Stanza name = package name. Git-tracked, edited by hand. -- `sb_builder/upstream.py` runs nvchecker through `QProcess` (as `remote.py` - runs `virsh`) with a fixed argv: `nvchecker -c <tmp>/nvchecker.toml - --logger json`. The temp dir holds a generated config: a `[__config__]` - with `keyfile` (from the GUI config, when the file exists), followed by the - repo file's text. No oldver, newver or `nvtake`: the comparison is against - the hints. -- Result: `{pkg: version}` and `{pkg: error}` from the JSON log. A package - without a result, or a failed run (exit status, stderr), shows as - `nvchecker error` with the reason in the tooltip. -- Upstream versions are stored with `-` as `_` (mkhint's `_normalize_version`): - `-` is illegal in a SlackBuild version, so upstream `2026-10-14` is - packaged `2026_10_14`. The comparison and `hint-update` use that form. -- A package is outdated when its upstream version differs from the hint - `VERSION`, or from the `.info` `VERSION` when the hint has none. Versions - are compared as strings; no ordering is attempted. - -GUI config gains `[hints] nvchecker_keyfile`, default -`~/.config/nvchecker/keys.toml` (expanded). A missing keyfile is not an -error: nvchecker runs without a token. - -## Part 2: helper (`sbb-remote` 0.5.0) - -### Config - -New key `manifest_packages` (below). `remote/config.example.toml` shows it. - -### Arguments - -- package: `PKGNAME` and present in the repo (as for slackrepo jobs). -- version: `^[A-Za-z0-9._+-]+$`, at most 64 characters. -- hint path: always `<hintdir>/<pkg>.hint`. If `find_hint` returns a - different file for the package (a hint earlier in slackrepo's search - order), every hint command refuses: "hint lives at <path>, not managed". -- packages with a bundled-dep manifest are named in config key - `manifest_packages`, default `["neovim", "openvino"]`. `hint-update` and - `hint-new` with a version refuse them: "bundled deps: use mkhint until - phase 4b". - -### Job kinds - -- `hint-update <pkg>=<ver>... [--no-dl]`: per package, in order: compute the - new text (apply the version, below), then back the hint up to `.bak` and - write atomically. A failure (download, missing `.info` VERSION) leaves the - hint and its old `.bak` untouched, logs `<pkg>: failed: <why>` and - continues with the next package. A package without a hint gets a new one - built as `hint-new` does. The last log line is - `SBB-RESULT {"ok": [pkg...], "failed": [[pkg, why]...]}`. Exit status 1 - when any package failed. -- `hint-new <pkg> [<ver>] [--no-dl]` (since 0.6.0 `hint-new <pkg>... [--no-dl]`, - no version, see the UI design): refuses when the hint exists. Builds - the hint from the `.info` as mkhint does, without phantom deps (see the - facts), then applies the version when given. No `.bak` (a stale - one is removed); revert means delete. - -These jobs never sign, so they do not check the signing key. Their argv is -the helper itself: `python3 sbb-remote _hint <kind> <args>`, validated again -there. Job ids now allow a hyphenated kind (`...-hint-update`). - -`job-result <id>`: the last `SBB-RESULT` object of a job's log. - -### Applying a version - -1. A hint without `VERSION` is first pinned from the `.info`: its - `VERSION`, `DOWNLOAD*` and `MD5SUM*` lines, plus `ARCH="x86_64"` when the - hint has no `ARCH`, are put in front of the hint's text. Old version = the - hint's `VERSION`. With no version given (`hint-new <pkg>`), steps 2 and 3 - are skipped. -2. Replace old by new everywhere in the file except inside `MD5SUM*` - values. When either contains `_`, also the `-` forms. When old contains - `.`, also its dotless form inside URLs, as a whole number (no digit before - or after). -3. Without `--no-dl`: for `DOWNLOAD`/`MD5SUM` and - `DOWNLOAD_x86_64`/`MD5SUM_x86_64`, split the values on whitespace (hint - values may continue over lines with `\`). Skip a pair whose value is empty, - `UNSUPPORTED` or `UNTESTED`. Download the first URL with `urllib` - (timeout 60 s, streamed into `hashlib.md5`, nothing kept on disk) and - replace its md5; keep the md5s of the other URLs. Write values back one - URL per line with ` \` continuation, as mkhint does. -4. With `--no-dl`: nothing is downloaded. Every `DOWNLOAD*` and `MD5SUM*` - assignment is removed (whole multi-line values) and `DOWNLOAD="no"` is - written, because `DOWNLOAD_$ARCH` would override it. - -Hint text is edited as text, not regenerated, so comments, order and other -keys stay as they are. An assignment is `KEY="..."` (the value may span -lines inside the quotes) or `KEY=word`, at line start. - -### Commands (answer at once, not jobs) - -- `hint-diff <pkg>`: `{"bak": text|null, "current": text|null}`. Both null - is an error ("no backup"). -- `hint-revert <pkg>`: when a `.bak` exists, move it over the hint (also - recreating a deleted hint); when there is no `.bak` and the hint exists, - delete the hint (reverting a `hint-new`). Returns `{"ok": true}`. -- `hint-delete <pkg>`: move the hint to `.bak`. Errors when there is no hint. - -`.bak` is one level deep and overwritten by each change, the mkhint -convention. - -### Version - -`VERSION` 0.5.0 in `remote/sbb-remote` and `sb_builder/__init__.py`. - -## Part 3: GUI - -### Hints screen (`sb_builder/hints_tab.py`, ribbon entry and `Open > Hints`) - -- Rows: every package with a stanza in `nvchecker.toml` or a hint in - `hintdir`. Columns: checkbox, package, hint version, `.info` version, - upstream, state. Versions come from the existing `inventory` call. -- States: `outdated`, `current`, `no stanza`, `not checked`, - `nvchecker error` (stderr in the tooltip). Filter chips per state and a - text filter on the name, as in the Packages tab. -- Buttons: - - **Check upstream**: runs Part 1 over the whole file, fills the upstream - column, checks the outdated rows. Results live for the session. - - **Update selected**: starts `hint-update pkg=ver ...` for the checked - rows that have an upstream version; a **No download** checkbox adds - `--no-dl`. - - **Delete hint**: confirm dialog, then `hint-delete` for the current row. -- Jobs go through the Jobs tab like every other job. - -### After a hint job - -When a `hint-update` or `hint-new` job ends, the GUI reads -its `SBB-RESULT` line and opens a dialog with one entry per changed package: -a unified diff (`difflib`) of `hint-diff`, and a **Revert** button per -package. Failed packages are listed with their reason. The dialog ends with -**Build these**, which starts `slackrepo update --preview` for the changed -packages that were not reverted (update builds unbuilt items too), and -**Close**. Revert is offered once per package: a second one would delete -the restored hint. After Revert or Delete the inventory is reloaded. - -Each diff is fetched when its package is selected. - -### Packages detail pane - -- No hint: **New hint**, with an optional version field and a **No - download** checkbox, starts `hint-new`. -- Hint present: **Open in Hints** selects the package in the Hints screen. - -## Error handling - -- Bad arguments, unmanaged hint paths and manifest packages are refused by - the helper before a job starts; the GUI shows the message. -- A download failure (HTTP status, timeout, DNS) fails that package only and - leaves its hint as it was. -- Writes are atomic (temp file in the same directory, `os.replace`). -- nvchecker missing or failing on the desktop affects only the upstream - column; the rest of the screen works. -- A job without an `SBB-RESULT` line (killed, lost) opens no dialog; the - log shows what happened and `hint-diff` still works per package. - -## Testing - -- `test_remote.py`: an `http.server` thread serves fixture files for md5. - Version swap forms (plain, `_`/`-`, dotless in URL only, md5 hex not - touched), multi-line `DOWNLOAD`/`MD5SUM` with continuation md5s kept, - `UNSUPPORTED` skipped, `--no-dl` removing `DOWNLOAD_x86_64` and writing - `DOWNLOAD="no"`, a download failure restoring the hint while the next - package still updates, `SBB-RESULT` contents, `hint-new` from `.info`, - a DELREQUIRES-only hint pinned from the `.info`, new -> revert, update -> diff -> - revert, delete -> revert, manifest package refusal, unmanaged hint path - refusal, bad version and package rejection. -- `test_upstream.py`: generated config, JSON log parsing, states (hint - version, `.info` fallback), the repo file has no `[__config__]`, `Checker` - against a fake `nvchecker` on `PATH` and a missing one. No network. -- `test_gui.py`: Hints screen with a fake remote and injected upstream - results (states, checked rows, Update selected argv), diff dialog Revert - call. - -## Deploy (asks first) - -1. `./install.sh --remote` with helper 0.5.0. -2. slackrepo_setup's sbshadow, which no longer runs `mkhint -F` on `--no`. -3. mkhint, nvchecker and `/root/.config/{mkhint,nvchecker}` stay on the VM - until phase 4b. diff --git a/docs/superpowers/specs/2026-10-20-sb-builder-ui-design.md b/docs/superpowers/specs/2026-10-20-sb-builder-ui-design.md deleted file mode 100644 index 8a6844d..0000000 --- a/docs/superpowers/specs/2026-10-20-sb-builder-ui-design.md +++ /dev/null @@ -1,163 +0,0 @@ -# sb-builder: one UI frame for every tab - -Date: 2026-10-20 -Status: approved -Builds on: phases 1, 2 and 4 (tabs, Jobs, Packages, Results, Hints) - -## Goal - -Every tab looks and works the same, and a multi-step task (check upstream, -update hints, preview, build) never sends the user hunting for the next -button in another place. Rules go into AGENTS.md so later tabs follow them. - -## What is wrong today - -- Action buttons sit at the bottom in Packages and Hints, in a side pane in - Results, at the top in Jobs. -- The ribbon opens tabs; tabs open and close. -- A step's follow-up lives elsewhere: Packages action -> auto switch to Jobs - -> Run it at the top of Jobs. Hints: Check upstream (bottom) -> tick -> - Update selected (bottom) -> diff dialog -> Build these -> Jobs -> Run it - (top). -- The Jobs Start menu name says nothing of what it holds. - -## Window frame - -Top to bottom: main ribbon, small ribbon, tab bar, the tab's view (its -filter bar first), status bar. - -- **Permanent tabs**: Dashboard (default), Packages, Hints, Results, Jobs, - created at startup, not closable. Each loads its data the first time it is - shown (Packages, Hints and Results call `inventory`/`results` then, not at - startup). Navigation: tab bar, Ctrl+PgUp/PgDn, Dashboard tiles (Packages, - Recent jobs, slackrepo results as today). The ribbon does not open tabs. - `File` menu unchanged. -- **Main ribbon**: `QToolBar`, 32 px icons, text under icon, the current - tab's actions in workflow order. Refilled on tab change. -- **Small ribbon**: `QToolBar` right under it, 16 px icons, text beside - icon, the tab's view controls. Hidden when the tab has none. -- Each tab exposes `actions` and `view_actions`, lists of `QAction` it owns, - so enabled and checked state survive tab switches. Icons are freedesktop - theme names with a `QStyle` fallback, as the ribbon does now. -- An action that does not apply is disabled with a tooltip saying why; - never hidden, so the ribbon does not shift. -- Filter bars (search, chips, `N shown · M selected` or `N of M`) stay at - the top of the view; their Refresh buttons move to the small ribbon. - -## Selection - -- Change actions act on the **ticked** rows; view actions (Show build log) - on the **highlighted** row. -- A click anywhere on a row toggles its tick (done for Packages and Hints). -- Ctrl+A ticks the visible rows, Ctrl+Shift+A unticks all (done); the - small ribbon's Select visible and Clear carry the same shortcuts. - -## Ribbons per tab - -| Tab | Main ribbon | Small ribbon | -|---|---|---| -| Dashboard | Refresh | none | -| Packages | Build, Update, Rebuild, Remove, Update all, New hint, Publish | Select visible, Clear, Refresh | -| Hints | Check upstream, Update selected, Delete hint | Select visible, Clear, No download (checkable), Refresh | -| Results | Show build log | Refresh | -| Jobs | Pull overlay (menu: one entry per overlay), Check shadowing, Run it, Kill | none | - -Disabled reasons (tooltips): - -- Build, Update, Rebuild, Remove: "tick packages first". -- New hint: "none of the ticked packages is without a hint". -- Update selected: "tick outdated packages after Check upstream". -- Delete hint: "none of the ticked packages has a hint". -- Show build log: "select a result with a log". -- Run it: "select a finished preview". -- Kill: "select a running job". -- Check upstream while running: "checking…". - -Update all, Publish, Pull overlay and Check shadowing are always enabled -(the helper refuses a second job with the busy answer, as today). - -Packages' detail pane keeps **Open in Hints** as a link (navigation, not an -action). Its New hint button moves to the ribbon. - -### New hint on ticked packages - -`hint-new` takes one or more packages and no version: `hint-new <pkg>... -[--no-dl]`. Each gets the `.info` pin; Update selected from upstream sets a -version later. The GUI sends no `--no-dl` here (Update selected has the -toggle); the helper keeps the flag. Every package must be without a hint, else the job is -refused before it starts, as today for one. Helper `VERSION` 0.6.0 in both -files, VM redeploy. The New hint dialog goes away. - -## Next-step dialogs - -- Shown only for jobs started from this GUI session: the main window keeps - the ids `job-start` returned. Jobs seen on reconnect or started elsewhere - only notify, as today. -- Non-modal, one per job, never reopened by polling. -- **Preview ended ok** (`slackrepo` with `--preview`): dialog titled - `Preview: <kind and args without --preview>`, with the whole job log - (`job-follow` from offset 0; the job has ended, so the stream closes by - itself), a **publish** checkbox (default `builds.publish`), **Run it** - (default button) and **Not now**. Run it goes through the signing-key - check (`start_signed`) and starts the real run. -- **sbpull or sbshadow preview ended ok with a shadow plan** (`shadow:` or - `unshadow:` lines, as `SHADOW_PLAN` today): same dialog, **Apply - shadowing** (`sbshadow --yes`, after a confirm: it changes the repo) and - **Not now**. Without a plan, no dialog. -- **Hint job ended** (`hint-update`, `hint-new`) or a delete: the existing - diff dialog; Build these starts `slackrepo update --preview`, whose end - opens the preview dialog. -- A real run ending, a failed or killed job: notification only. -- **Run it** in the Jobs ribbon, with a finished preview selected, reopens - that preview's dialog (also for a shadowing preview), so Not now loses - nothing. The Jobs top bar (Start, Apply shadowing, Run it, publish, Kill) - goes away. - -The hugo flow becomes: Hints, Check upstream -> Update selected -> diff -dialog, Build these -> preview dialog, Run it. No tab switch. - -## Jobs tab signal - -- Starting a job no longer switches to Jobs; the user stays on the tab. -- While a job runs, the Jobs tab shows a running icon (`system-run`) and - reads "Jobs (running)": the tab style sheet's text color would override a - per-tab color. -- When it ends, the icon becomes the end status (`✔` ok, `✘` failed, - killed, lost) and stays until the Jobs tab is shown, then clears. -- The status bar job button stays. - -## AGENTS.md: UI guidelines - -A new section, binding for every tab: - -1. Tabs are permanent; each loads lazily on first show. -2. Main ribbon: the tab's actions in workflow order. Small ribbon: view - controls. Nothing actionable lives inside a view except navigation links. -3. Change actions act on ticked rows, view actions on the highlighted row. - A row click toggles its tick; Ctrl+A / Ctrl+Shift+A select visible / - clear. -4. An inapplicable action is disabled with a tooltip saying why, never - hidden. -5. When a job this session started ends, its next step opens as a - non-modal dialog where the user is; the Jobs tab shows activity; a - dismissed step can be taken again from the ribbon. -6. A modal confirm only for destructive or outward steps (delete hint, - publish, apply shadowing). - -## Testing - -`test_gui.py` with the fake remote, offscreen: - -- ribbon contents per tab (action texts in order) after switching tabs; -- disabled states and tooltips (nothing ticked, no preview selected, no job - running); -- tabs are not closable and present from the start; data loaded on first - show only (no `inventory` call before Packages or Hints is shown); -- a preview this session started ends -> dialog -> Run it starts the real - run through the signing check; Not now, then Jobs' Run it reopens it; -- a preview not started by this session ends -> no dialog; -- Jobs tab icon running -> end status -> cleared on showing Jobs; -- New hint on two ticked packages -> `hint-new a b`. - -`test_remote.py`: `hint-new` with several packages, refusing a version and -a package that has a hint. |
