On the pacman backend, an installed AUR helper (paru, else yay;
--aur-helper=paru|yay|none, --no-aur) adds the AUR updates to the review
list. The helper never runs as root: when the tool is root through sudo it
runs as SUDO_USER (runuser -u, else sudo -u, with that user's HOME, from
that home); unprivileged it runs as is; a root login gets no AUR, with a
warning. The banner names the helper ("+ AUR via paru").
- listing: <helper> -Qua (also with --no-refresh), rows with repository
"aur"; [ignored] is shown [held]; a failing -Qua warns and the session
goes on with the repository rows
- every row has a marker column after the number: "*" for AUR rows, a
space otherwise (on every backend)
- N?? merges pacman -Qi with <helper> -Sia; N?, N+, Nx stay with pacman;
N- excludes through the helper's --ignore
- <enter>: the repository step exactly as before, then <helper> -Sua
--ignore <excluded> as the invoking user (--noconfirm only when stdin is
not a terminal); pacman is skipped when only AUR rows are left; a helper
failure is reported and its status returned; --dry-run prints it
- other backends never run a helper; --aur-helper is ignored with a warning
paru 2.1.0 and yay 13.0.1 formats verified in discovery/pacman.md section
15 (proof: discovery/pacman/aur.sh). Unit tests replay the recorded output
(fake paru, yay, runuser, getent); the archlinux E2E run installs the
foreign su-dummy-aur and drives a fake paru through a fake sudo built on
passwordless su for wheel.
337 lines
24 KiB
Markdown
337 lines
24 KiB
Markdown
# skeptical-updater — design
|
|
|
|
A distro-agnostic, single-file bash applet for a *manual, conscious* update cycle.
|
|
It wraps the native package manager (apt, dnf/yum, pacman/pman, zypper), lists
|
|
upgradable packages, lets the user interrogate each one, and only then runs the
|
|
native upgrade. Packages the user has explicitly acknowledged are remembered in a
|
|
statefile and never questioned again.
|
|
|
|
## Goals
|
|
|
|
- One file, no runtime dependencies beyond bash >= 4.3 and the distro's package manager.
|
|
- Never reimplement package management: every action shells out to the native tool.
|
|
- Deterministic, testable: `--dry-run` prints the commands it would run; stdin is
|
|
the command channel (works piped, no TTY needed).
|
|
- Statefile is plain text, hand-editable, atomic writes.
|
|
|
|
## Backend detection
|
|
|
|
Order (first match wins, overridable with `--backend=<name>`):
|
|
|
|
| backend | detected by | notes |
|
|
|----------|-----------------------------|--------------------------------------------|
|
|
| `pacman` | `pacman` in PATH | `pman` present => label shown as `pman`, same commands; honor pacman `IgnorePkg` (`pacman -Qu` marks them `[ignored]`); the refresh syncs a throwaway `--dbpath` copy; at `<enter>` its reviewed databases are installed and applied with `pacman -Su`; AUR packages through `paru` (else `yay`) when installed, see "AUR" below |
|
|
| `apt` | `apt-get` in PATH | Debian/Ubuntu |
|
|
| `dnf` | `dnf` in PATH (else `yum`) | dnf4 (EL9) and dnf5 (EL10) differences hidden in the backend |
|
|
| `zypper` | `zypper` in PATH | `dist-upgrade` on Tumbleweed (ID=opensuse-tumbleweed), `update` on Leap; on Tumbleweed the review list is the `dist-upgrade --dry-run` plan |
|
|
|
|
Each backend implements the same function set (see "Backend contract").
|
|
|
|
### AUR (pacman backend only)
|
|
|
|
| helper | detected by | overridden by |
|
|
|--------|-------------|---------------|
|
|
| `paru` | `paru` in PATH (first) | `--aur-helper=paru` (an error when it is not in PATH) |
|
|
| `yay` | `yay` in PATH (when there is no paru) | `--aur-helper=yay` (idem) |
|
|
| none | neither installed | `--aur-helper=none`, `--no-aur` |
|
|
|
|
The helper is detected with the backend (before any sudo re-exec). The banner then reads
|
|
`Backend: pman (Arch Linux) + AUR via paru`. The other backends never look a helper up:
|
|
`--aur-helper=paru|yay` is accepted there and ignored with a warning (`--aur-helper=paru is
|
|
ignored: AUR helpers only apply to the pacman backend ...`), `--no-aur`/`--aur-helper=none` silently.
|
|
|
|
The helper never runs as root (paru refuses to install AUR packages as root, makepkg refuses to
|
|
build as root). Decided once, after the root handling (the banner decides it; the listing runs in
|
|
subshells):
|
|
|
|
| we are | the helper runs |
|
|
|--------|-----------------|
|
|
| not root (`list --no-refresh`, `info`, `show`, `--dry-run`) | as we are: `paru ARGS` |
|
|
| root, `SUDO_USER` names a user whose uid (`getent passwd`, else `/etc/passwd`) is not 0 - our sudo re-exec, or any sudo | as that user, from that user's home: `runuser -u "$SUDO_USER" -- env HOME=<home> paru ARGS`, `sudo -u "$SUDO_USER" -- env HOME=<home> ...` when there is no runuser |
|
|
| root without `SUDO_USER` (a root login), `SUDO_USER` root or unknown, neither runuser nor sudo | not at all: no AUR row, one warning (`aur: paru is never run as root and there is no invoking user (SUDO_USER is not set): AUR packages are neither listed nor upgraded ...`) |
|
|
|
|
Helper commands (formats verified in `discovery/pacman.md` section 15, paru 2.1.0 and yay 13.0.1):
|
|
|
|
| need | command | notes |
|
|
|------|---------|-------|
|
|
| list | `<helper> -Qua` | `name old -> new`, then ` [ignored]` (paru; yay leaves `IgnorePkg` packages out) or yay's age (` [122d13h]`, ignored); devel packages show `latest-commit` as the new version; escape sequences and other lines are dropped. Exit 1 with nothing on stdout and stderr = nothing to upgrade; any other failure: the helper's stderr lines as warnings, then `aur: <helper> -Qua failed, AUR packages not listed`, and no AUR row. Asked on every run, also with `--no-refresh` (the helper always compares with the AUR RPC) |
|
|
| `N??` candidate | `<helper> -Sia PKG` | for a package in no repository (`pacman -Sl`); paru's record aligns its keys like `pacman -Qi`, yay's are wider (re-padded by the merge) |
|
|
| upgrade | `<helper> -Sua [--noconfirm] [--ignore a,b]` | see "After `<enter>`" |
|
|
|
|
## Statefile
|
|
|
|
Directory: `/var/lib/skeptical-updater/` (override: `SKEPTICAL_UPDATER_STATE_DIR`).
|
|
Created `0755 root:root` on first write. The tool runs as root (it must, to upgrade);
|
|
when not root it re-execs itself through `sudo` if available.
|
|
|
|
File: `trusted.tsv` — tab-separated, UTF-8, LF, one record per line, sorted and
|
|
unique on `(backend, package)`. Lines starting with `#` are comments and are kept.
|
|
|
|
```
|
|
# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)
|
|
apt nodejs user 2026-10-09T09:40:12Z
|
|
apt libnode115 dep:nodejs 2026-10-09T09:40:12Z
|
|
```
|
|
|
|
- `reason` is `user` for a package the user confirmed with `N+`, or `dep:<root>`
|
|
for a package pulled in as part of the installed dependency closure of `<root>`
|
|
at the time of confirmation.
|
|
- `backend` is recorded so the file stays correct on hosts with more than one
|
|
package manager and stays readable when copied between hosts.
|
|
- Writes are atomic: write to `trusted.tsv.tmp.$$` in the same directory, `mv` over.
|
|
- Writes happen under a lock (`trusted.lock`: `flock(1)` when available, else an atomic
|
|
`mkdir` lock: a directory created under a temporary name with the owner's pid inside and
|
|
renamed into place; a lock whose owner is gone - a process of another user counts as alive -
|
|
is taken over by renaming it away first and checking its pid again there, so two waiters can
|
|
never remove a live lock; never busy-waited on; leftovers of crashed acquirers are removed).
|
|
At save time the
|
|
file is re-read and this session's additions/removals are replayed onto it, so a `trust` run
|
|
in another terminal meanwhile is not lost.
|
|
- Comment lines are kept, in their order, at the top of the file when it is rewritten;
|
|
malformed lines are kept verbatim (with a warning) in the same block.
|
|
- A package is "trusted" if a row with the current backend and that name exists.
|
|
Reason does not matter for filtering.
|
|
|
|
Why a stored closure instead of computing it every run: closure computation across
|
|
40+ upgradable packages on every run is slow on apt/rpm; computing it once at `N+`
|
|
time and storing `dep:<root>` rows keeps every run O(upgradable). A new dependency
|
|
that appears later is *new to the system* and deserves to be questioned once;
|
|
`skeptical-updater refresh` recomputes closures for all `user` roots when the user
|
|
wants to re-trust them wholesale.
|
|
|
|
No other state, except two crash-recovery files that only exist while an upgrade runs (or
|
|
after one was killed): `holds.added`, the apt hold journal (used under `holds.lock`, which is
|
|
kept until the holds are really released), and `zypper-private`, the path of the private zypper
|
|
configuration (used under `zypper.lock`). Both are replayed/removed at the start of the next root
|
|
run, before anything is listed - only when their lock is free, i.e. not while another run uses
|
|
them. Per-run exclusions (`N-`) are
|
|
intentionally not persisted: a skeptical user wants to be asked again next time. Permanent holds belong to the
|
|
package manager (`apt-mark hold`, `IgnorePkg`, `zypper addlock`, dnf `exclude=`).
|
|
|
|
## Interactive session
|
|
|
|
```
|
|
$ sudo skeptical-updater
|
|
Backend: apt (Debian GNU/Linux 12)
|
|
Refreshing package lists... done.
|
|
12 upgradable, 9 trusted (hidden, use --all to show), 3 to review:
|
|
|
|
[1] nodejs 18.19.0+dfsg-6 -> 18.20.4+dfsg-1 bookworm-security
|
|
[2] libnode108 18.19.0+dfsg-6 -> 18.20.4+dfsg-1 bookworm-security
|
|
[3] zsh 5.9-4 -> 5.9-4+deb12u1 bookworm
|
|
|
|
<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever
|
|
> 1?
|
|
```
|
|
|
|
Command grammar (one line, whitespace-separated tokens, each `N` is the number in brackets):
|
|
|
|
| token | action |
|
|
|--------|---------------------------------------------------------------------------|
|
|
| empty line | proceed: run the native upgrade with the current exclusions |
|
|
| `N?` | print three lines, none indented: `PKG: <one-line description>`, `reason: <r>` with `<r>` = `explicitly installed`, `installed as a dependency`, `unknown` (the backend cannot tell) or `not installed` (also, without asking the backend, for a row whose old version is `-`), and `required by: <a, b, ...>` - the reverse dependencies, installed packages only - or `required by: nothing` |
|
|
| `N??` | print the full native info: the installed record merged with the upstream candidate (see "Full info" below); zypper: `zypper info --requires` as it is |
|
|
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with `-` marker |
|
|
| `Nx` | purge it now, following pacman's `-Rns` principle on every backend: if any installed package still depends on it, refuse and print who (the reverse dependencies `N?` shows) - never cascade; otherwise simulate the native purge of it together with its now-unneeded dependencies, show that removal set ("This also removes: ...") and ask `y/N`. As a safety check, a simulated set containing anything that is neither the target nor in its installed dependency closure is refused too. Only a purge that ran removes rows (of every package it removed). A row whose old version is `-` (a new dependency, not installed) cannot be purged |
|
|
| `N+` | trust it: write it and its installed dependency closure to the statefile; the row leaves the list (with `--all` it stays, marked `=`). Rows that this trust hides are re-included if they were excluded |
|
|
| `*+` / `*-` | apply `+` / `-` to every listed package; `*-` is an all-or-nothing toggle: when every listed row is already excluded it re-includes them all |
|
|
| `l` | redraw the list |
|
|
| `q` | quit without upgrading (exit 0) |
|
|
| `h`, `?` | help |
|
|
|
|
List format: a marker column (`-` excluded, `=` trusted with `--all`, else a space), the number in
|
|
brackets right-aligned to the widest one, then one column that is `*` for an AUR row and a space for
|
|
every other row (on every backend: the columns stay aligned), a space, then name, old version, `->`,
|
|
new version, repository (`aur` for AUR rows) and the flags (`[held]`):
|
|
|
|
```
|
|
[1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy
|
|
-[2]* su-dummy-aur 1.0-1 -> 2.0-1 aur
|
|
```
|
|
|
|
On AUR rows `N?`, `N+` and `Nx` act through pacman like on any installed package; `N??` merges
|
|
`pacman -Qi` with `<helper> -Sia PKG`; `N-` excludes the row through the helper's `--ignore`.
|
|
|
|
Numbers are stable for the whole session (removed rows leave a gap, never renumber).
|
|
All tokens of a line are validated first: an unknown token or a number that is not listed
|
|
prints an error, runs nothing of that line and re-prompts. EOF on stdin aborts without
|
|
upgrading (exit 2) so a piped run can never upgrade by accident unless it sends an empty
|
|
line. When purges leave no row at all, the session ends ("Nothing left to upgrade.", exit 0).
|
|
|
|
Line input. When stdin is a terminal every line is read with bash's readline,
|
|
`IFS= read -e -r -p PROMPT`, with readline's defaults and nothing more: cursor movement and editing
|
|
(Left/Right, Home/End, Ctrl-A/E, Backspace/Delete) and Up/Down over an in-memory history of this
|
|
session's command lines. A line with a non-blank character is added with `history -s` (empty
|
|
lines, the proceed command, are not); Down past the newest entry restores the line typed before Up
|
|
(readline's saved line); Up at the oldest entry and Down on the new line ring the bell. The `Nx`
|
|
`y/N` answer is read the same way and never added. Nothing persists: `HISTFILE` is unset at
|
|
startup and `history -r`/`-w`/`-a` are never used (a non-interactive bash neither loads nor saves
|
|
history by itself). readline draws the prompt and the line on stderr, as bash does. Any other
|
|
stdin keeps the plain `IFS= read -r` with the prompt on stdout and each line echoed after it, so
|
|
piped runs and their logs are unchanged.
|
|
|
|
Full info (`N??`, `show PKG`). The first line is `# lines starting with > come from the upstream
|
|
candidate: a differing value, or a field the installed record lacks`; then the installed record in
|
|
its native key/value layout, as the base. After each field whose candidate value differs, the
|
|
candidate's line follows, prefixed with `> ` (same key formatting); fields only the candidate has
|
|
are appended at the end the same way; fields only the installed record has stay; identical fields
|
|
print once. A field starts on a line that begins with a non-blank character and holds a `:` (the
|
|
key is the text before it); lines beginning with a blank continue it, so multi-line values (pacman
|
|
`Optional Deps`, dnf `Description`, RFC-822 continuation lines) are compared as blocks, every line
|
|
trimmed, and a differing candidate block gets `> ` on every line. `Description-xx` (apt Translation
|
|
files) pairs with `Description`; on apt a one-line candidate `Description` equal to the first
|
|
installed line counts as identical (the archives carry only the short description, the long text
|
|
is in the Translation files). When both records align their `:` in one column but in different
|
|
ones (dnf5: 16 installed, 15 available), the candidate lines are re-padded to the installed one's.
|
|
|
|
| backend | installed record | upstream candidate |
|
|
|---------|------------------|--------------------|
|
|
| pacman | `pacman -Qi PKG` | `pacman -Si REPO/PKG`, REPO = the first repository `pacman -Sl` lists PKG in (as for the review list); a package in no repository: the AUR record, `<helper> -Sia PKG`, when a helper is in use (should it not merge, `pacman -Qi` is followed by the AUR record with every line marked `> `) |
|
|
| apt | `dpkg-query -s PKG` (Status, Conffiles, always the long description; `apt-cache show PKG=<installed>` gives the archive's record when that version is still listed: no Status/Conffiles, short description only); a `Status` of `not-installed`/`config-files` = not installed | `apt-cache show --no-all-versions PKG` |
|
|
| dnf | `dnf -q info --installed PKG` | `dnf -q info --upgrades PKG` (installed architecture only; `--available` also lists other architectures and, for an up-to-date package, other builds); not installed: `--available` |
|
|
| zypper | - | - (`rpm -qi` and `zypper info` share almost no keys - Version vs version-release, Arch, byte sizes, source names, description layout - and zypper cannot print the installed record by itself: `zypper info -r @System` is refused) |
|
|
|
|
A package that is not installed (a row whose old version is `-` is not asked) shows the candidate
|
|
alone under `# not installed: this is the upstream candidate`. Anything else - no candidate (an
|
|
up-to-date dnf package, a local-only pacman package), not exactly one record on each side (two
|
|
installed architectures), zypper - prints `<be>_full_info` unchanged. `dnf info` headings
|
|
("Installed Packages", ...) are dropped. Verified formats: `discovery/{pacman,apt,rpm}.md`.
|
|
|
|
Flags shown after a row: `[held]` = the package manager will not upgrade it on its own: on apt
|
|
a hold (`apt-mark hold`) or a package apt keeps back (the "kept back" block of
|
|
`apt-get -s --with-new-pkgs upgrade`), on pacman an `IgnorePkg` (`[ignored]` in `pacman -Qu`).
|
|
dnf excludes/versionlocks and zypper locks hide the package from the native listing instead.
|
|
Notes after the rows report what the backend says beyond them (new dependencies or
|
|
replacements the upgrade installs, packages a Tumbleweed `dist-upgrade` removes); with no row
|
|
but such pending work the prompt is still offered ("0 to review, but ... reports pending
|
|
changes").
|
|
|
|
After `<enter>`:
|
|
1. Build the exclusion set = `N-` rows.
|
|
2. Apply native per-run exclusion (`pacman --ignore a,b`, `dnf --exclude`,
|
|
apt temporary `apt-mark hold` of the packages not already held, journalled first and
|
|
released afterwards - also on failure, Ctrl-C, TERM, HUP, QUIT, a closed stdout - so the hold
|
|
set is exactly the prior one; zypper `addlock` in a private configuration whose lock file is
|
|
a copy of the effective one, so the real lock set is never written).
|
|
3. Run the native upgrade non-interactively, streaming its output, on exactly the metadata that
|
|
was reviewed: dnf `--setopt=*.metadata_expire=-1`, zypper `--no-refresh`; apt never
|
|
refreshes on upgrade. pacman: (a) `pacman -Sup --noconfirm --ignore ... --dbpath <throwaway>`
|
|
checks the reviewed transaction - if it fails (an ignored package another upgrade needs, a
|
|
conflict) its output is shown and the tool exits with its status, nothing changed; (b) the
|
|
throwaway `*.db` and `*.db.sig` files are installed into the system's sync directory under
|
|
pacman's own lock (`db.lck`, created atomically; an existing one = another pacman: nothing
|
|
changed). A system database that changed since the review and is newer than the reviewed
|
|
copy is never overwritten: the tool refuses ("package databases changed since the review
|
|
(<repos>)") with exit status 2 and changes nothing; identical ones are skipped. The install is
|
|
all-or-nothing: everything is copied to temporary names first, then all renames happen with
|
|
the signals ignored, each `.db.sig` right before its `.db`; `db.lck` is removed right before
|
|
(c) `pacman -Su --noconfirm --ignore ...` applies them, no further sync. If (c) fails or is
|
|
interrupted the system's sync database is newer than its installed packages: the tool (or its
|
|
exit handler) says so; finish with `pacman -Su` (or run the tool again) before any
|
|
`pacman -S`. With `--no-refresh` (or a failed refresh)
|
|
plain `pacman -Su` applies the existing databases. Throwaway databases carry their owner's
|
|
pid in the name (`/tmp/skeptical-updater-db.<pid>.XXXXXX`); those of runs that are gone are
|
|
removed at the start of the next root run.
|
|
AUR rows (pacman backend): pacman gets `--ignore` with the excluded repository rows only. When
|
|
no repository row is left to upgrade (every one excluded or `[held]`, no pending note) but AUR
|
|
rows are, (a)-(c) are skipped ("No repository package to upgrade: pacman is not run, only
|
|
paru."). Otherwise (a)-(c) run first, and only after they succeeded (d)
|
|
`<helper> -Sua --ignore <every excluded row, repository and AUR>` runs as the invoking user (see
|
|
"AUR"), when an AUR row that is neither excluded nor `[held]` remains: with stdin a terminal the
|
|
helper gets it and asks as usual (paru's PKGBUILD review, yay's menus, its sudo password);
|
|
otherwise `--noconfirm` and stdin from `/dev/null` (yay exits 1 at its first menu on a closed
|
|
stdin). A helper failure is reported as `the AUR upgrade failed: paru exited with status N (the
|
|
repository packages were upgraded)` and its status is the exit status; `--dry-run` prints the
|
|
helper's command line.
|
|
4. Restore holds/locks, exit with the package manager's exit code.
|
|
|
|
Trusted packages are upgraded silently along with everything else; they are only
|
|
hidden from the questioning list.
|
|
|
|
## CLI
|
|
|
|
```
|
|
skeptical-updater [options] [command]
|
|
|
|
commands:
|
|
(none) interactive review + upgrade
|
|
list print the review list and exit (no prompt)
|
|
trusted print the statefile
|
|
trust PKG... add PKG and its installed dependency closure to the statefile
|
|
untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG)
|
|
refresh recompute dependency closures for every `user` root
|
|
info PKG same as N?
|
|
show PKG same as N??
|
|
|
|
options:
|
|
--backend=NAME force apt|dnf|pacman|zypper
|
|
--all also list trusted packages (marked with `=`)
|
|
--no-refresh skip the metadata refresh step
|
|
--dry-run print native commands instead of running mutating ones
|
|
--state-dir=DIR override /var/lib/skeptical-updater
|
|
--no-color disable ANSI colours (also honours NO_COLOR, non-tty stdout)
|
|
--aur-helper=NAME pacman backend: paru|yay|none (default: paru, else yay, when installed)
|
|
--no-aur same as --aur-helper=none
|
|
--version, --help
|
|
```
|
|
|
|
Exit codes: 0 ok (also `q`), 1 usage/backend error, 2 aborted (end of input),
|
|
otherwise the native package manager's exit code from the upgrade step.
|
|
|
|
When root is needed (refresh, upgrade, purge, writing a state directory the user cannot
|
|
write) and the user is not root, the tool re-runs itself as
|
|
`sudo -- /absolute/path/to/itself --sudo-reexec ARGS...` - the file itself, so a sudoers rule
|
|
for it keeps matching; only a copy that is not executable is run as
|
|
`sudo -- "$BASH" /absolute/path --sudo-reexec ARGS...`. The hidden `--sudo-reexec` (accepted
|
|
only as the first argument, never forwarded) makes a re-run that is still not root fail instead
|
|
of looping. Without sudo it fails with a clear message.
|
|
|
|
## Backend contract
|
|
|
|
Every backend is a set of bash functions with a common prefix. All output is
|
|
`LC_ALL=C`. All functions must work without a TTY.
|
|
|
|
```
|
|
<be>_refresh # update metadata; non-fatal on failure (warn)
|
|
<be>_upgradable # stdout: name<TAB>old<TAB>new<TAB>repo<TAB>flags (flags: space-separated words:
|
|
# "held", "aur" (pacman: an AUR row, shown with "*"); old "-" = new package)
|
|
# plus "#<note>" lines for pending work that is not a row
|
|
<be>_describe PKG # stdout: one-line summary
|
|
<be>_full_info PKG # stdout: native full dump (candidate version if available)
|
|
<be>_installed_record PKG # optional (pacman, apt, dnf): stdout: ONE native record of the installed package
|
|
<be>_candidate_record PKG installed|not-installed # optional, with the above: ONE native record of the candidate
|
|
<be>_rdepends PKG # stdout: installed packages requiring PKG, one per line
|
|
<be>_reason PKG # stdout: explicit | dependency | unknown | not-installed (how PKG got installed)
|
|
<be>_closure PKG # stdout: installed recursive dependency closure of PKG (excluding PKG), one per line
|
|
<be>_purge_plan PKG # stdout: every package a purge of PKG and its now-unneeded dependencies removes
|
|
# (simulation, no cascade); non-zero = refused
|
|
<be>_purge PKG... # non-interactive native purge/remove of exactly the planned packages
|
|
<be>_upgrade EXCLUDED... # non-interactive native upgrade skipping EXCLUDED (handles hold/lock save+restore;
|
|
# pacman: then the AUR helper, see "After <enter>")
|
|
<be>_label # "apt" / "pman" / ...
|
|
```
|
|
|
|
Mutating calls go through `run_cmd` which honours `--dry-run` (prints `+ cmd` to
|
|
stdout instead of executing).
|
|
|
|
`<be>_reason` sources (read-only, unprivileged, verified in `discovery/*.md`): apt
|
|
`apt-mark showauto PKG` (dependency) / `apt-mark showmanual PKG` (explicit), a package in neither
|
|
list is explicit when dpkg has it installed; pacman the `Install Reason` of `pacman -Qi`; dnf
|
|
`repoquery --installed --queryformat '%{reason}'` (`user`/`group`/dnf5 `External User` = explicit,
|
|
`dependency`/`weak-dependency` = dependency, dnf4 `unknown` - plain `rpm -i` - stays unknown, no
|
|
output = not installed); zypper the `Installed` line of `zypper --no-refresh info`
|
|
(`Yes (automatically)` / `Yes` / `No`), and when zypper gives no answer (its lock is busy) `rpm -q`
|
|
plus libzypp's `/var/lib/zypp/AutoInstalled`.
|
|
|
|
## Layout
|
|
|
|
```
|
|
skeptical-updater the applet (single bash file)
|
|
makefile install / lint / test / docker targets (no LLM needed)
|
|
docs/DESIGN.md this file
|
|
discovery/ verified per-family command notes and fixture scripts
|
|
tests/unit/*.bats bats-core tests with a fake backend (no root, no docker)
|
|
tests/e2e/*.bats bats-core tests run inside each distro container against real package managers
|
|
tests/docker/ images list, snapshot/cleanup scripts, per-distro Dockerfiles
|
|
```
|