SysDeck 4.4 - Standalone Edition: consolidates the day-to-day work of a Linux operations team in a single webui
This commit is contained in:
parent
3fdbf6d3f0
commit
cd522bedcd
14
Makefile
14
Makefile
|
|
@ -30,7 +30,7 @@
|
|||
# Distro support: Arch Linux, Debian/Ubuntu, Fedora/RHEL/CentOS.
|
||||
|
||||
PACKAGE := sysdeck
|
||||
VERSION := 0.4.3
|
||||
VERSION := 0.4.4
|
||||
LIB_DIR := $(DESTDIR)/usr/lib/$(PACKAGE)
|
||||
PYTHON_DIR := $(LIB_DIR)/bridge
|
||||
SHARE_DIR := $(DESTDIR)/usr/share/$(PACKAGE)
|
||||
|
|
@ -447,16 +447,16 @@ uninstall-branding:
|
|||
@echo ">>> Starting SysDeck Web Edition on :3000 (Ctrl+C stops next; fester keeps running)"
|
||||
cd web && bun run dev
|
||||
|
||||
# master: rebuild the master tarball from this tree (cockpit + web + fester)
|
||||
# master: rebuild the master tarball from this tree (cockpit + web + fester + klanker-gate)
|
||||
master:
|
||||
@echo ">>> Building $(PACKAGE)-$(VERSION)-master.tar.bz2 (cockpit + web + fester)"
|
||||
@echo ">>> Building $(PACKAGE)-$(VERSION)-master.tar.bz2 (cockpit + web + fester + klanker-gate)"
|
||||
tar cjf $(PACKAGE)-$(VERSION)-master.tar.bz2 \
|
||||
--exclude='__pycache__' --exclude='*.pyc' --exclude='*.tar.bz2' \
|
||||
--exclude='*node_modules*' --exclude='*.next' \
|
||||
--exclude='*node_modules*' --exclude='*.next' --exclude='*.tsbuildinfo' \
|
||||
--exclude='*public/download*' --exclude='*.db' --exclude='*.db-*' \
|
||||
--transform 's,^,$(PACKAGE)-$(VERSION)-,' \
|
||||
--transform 's,^,$(PACKAGE)-$(VERSION)-master/,' \
|
||||
bridge plugins shared tests packaging compat standalone-plugins \
|
||||
prometheus scripts firewall docs web \
|
||||
Makefile README.md QUICKSTART.md BLOG.md LICENSE QA.md THIRD_PARTY.md \
|
||||
prometheus scripts firewall docs web klanker-gate \
|
||||
Makefile README.md QUICKSTART.md BLOG.md LICENSE QA.md worklog.md THIRD_PARTY.md \
|
||||
sysdeck-diagnose.sh cockpit-smoke-test.sh sysdeck-uninstall.sh
|
||||
@echo ">>> $(PACKAGE)-$(VERSION)-master.tar.bz2 ready"
|
||||
|
|
|
|||
75
QA.md
75
QA.md
|
|
@ -1207,6 +1207,81 @@ and this, the console/host pair is 100% aligned.
|
|||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# MoE Quality Assurance Pass — v0.4.4 (package parity + the blog essay)
|
||||
|
||||
## v0.4.4 QA — ten managers on both editions, parsers locked by fixtures
|
||||
|
||||
**Reviewer panel (MoE):** backend coder · JS/TS expert · algorithms
|
||||
specialist (parser robustness) · technical writer (blog pattern)
|
||||
**Date:** 2026-09-12
|
||||
|
||||
### What was audited and fixed
|
||||
|
||||
- **Parity gap closed:** `bridge/packages.py` (cockpit edition) carried
|
||||
only pacman/dnf/apt while the web console carried ten backends.
|
||||
The cockpit bridge now runs the identical step-down — pacman →
|
||||
emerge (corroborated by `/var/db/pkg`) → lunar → sorcery → xbps
|
||||
(probed via `xbps-query`; Void ships no bare `xbps` binary) → apk →
|
||||
zypper → dnf → yum → apt — with `shutil.which` presence probes (no
|
||||
`--version` child processes) and the same corroboration rules.
|
||||
- **Silent-fabrication bugs found by fixture tests and fixed on BOTH
|
||||
editions:** (1) the emerge update regex captured the class bracket
|
||||
`]` as the "atom" — portage pads the class field with spaces, so
|
||||
every update row was dropped and Gentoo hosts silently showed "no
|
||||
updates"; the capture now anchors after the bracket. (2) zypper
|
||||
tables were parsed positionally, but zypper prefixes its tables
|
||||
with status/repository columns that vary by subcommand and release;
|
||||
parsing now locates `Name`/`Current`/`Available` from the header row
|
||||
(separator and repeated-header rows filtered). (3) the xbps search
|
||||
regex required a repository prefix that `xbps-query -Rs` rows do
|
||||
not consistently carry — searches silently returned zero rows; the
|
||||
prefix is now optional. (4) the web emerge info lookup resolved
|
||||
only bare names — category-qualified atoms returned null; both
|
||||
lookup shapes now resolve.
|
||||
- **Honest capability reporting:** lunar has no `lvu` update-preview
|
||||
subcommand — its summary carries the note instead of a zero count
|
||||
that reads as "all current", and single-module update refuses with
|
||||
the real instruction. Mutation argv for all ten managers lives in
|
||||
one `MUTATION_CMDS` table (`emerge --unmerge`, `cast`/`dispel`,
|
||||
`lin`/`lrm`, `xbps-install -y`, `zypper --non-interactive`, ...)
|
||||
shared by install/remove/update/update-all/dry-run — the dry-run
|
||||
preview and the executed command cannot diverge.
|
||||
- **Surface hygiene:** polkit `org.sysdeck.packages.modify` exec-path
|
||||
annotations extended to the ten managers (and a latent `--` inside
|
||||
an XML comment in the policy file fixed — strict parsers rejected
|
||||
it); the cockpit packages panel's unavailable-message and header
|
||||
narrate the ten-manager reality and render `summary.updatesNote`;
|
||||
`bridge/__init__.py`'s DistroId/PkgManager maps cover the new
|
||||
distros; churn-narration docstrings in the touched files rewritten
|
||||
as decisive rules.
|
||||
- **BLOG.md rebuilt as a long-form engineering essay** (the shellm
|
||||
blog pattern): title, italic deck, context narrative, roadmap
|
||||
paragraph, decision-organized sections (auth via PAM, one catalog
|
||||
two frontends, the compiler-enforced zero-demo contract, the
|
||||
ten-manager step-down, the firewall privilege discipline,
|
||||
performance without fabrication), a canonical numbered workflow, and
|
||||
an attribution footer. Every file path, flag, token format, and
|
||||
count in the essay verified against the source. Release notes
|
||||
content no longer lives in BLOG.md; history stays in QA.md and
|
||||
worklog.md, and README's pointers say so.
|
||||
|
||||
### Verification
|
||||
|
||||
`python3 -m py_compile` across bridge/*.py · fixture suite
|
||||
`scripts/test_packages_backends.py` 10/10 checks · new unittest class
|
||||
`TestPackagesBackends` 13/13 in `make check` (detection order + xbps
|
||||
probe, emerge corroboration via mocked `shutil.which`, zypper
|
||||
header-locate across three layouts, emerge bracket-anchored regex,
|
||||
xbps prefix-optional regex, MUTATION_CMDS coverage incl. lunar's
|
||||
honest absence, real argv spot-checks, honest lunar summary, no-sudo
|
||||
source guard) · `node --check` on the packages panel · polkit policy
|
||||
XML validated with a strict parser · `tsc --noEmit` clean and eslint
|
||||
clean on the web tree (full dep install) · version sync at 0.4.4
|
||||
across all release surfaces · master tarball rebuilt via
|
||||
`make master`.
|
||||
|
||||
# MoE Quality Assurance Pass — v0.4.3 (the hardened release)
|
||||
|
||||
## v0.4.3 QA — multi-expert audit, findings landed
|
||||
|
|
|
|||
|
|
@ -434,6 +434,38 @@ A multi-expert audit hardened every axis of the console:
|
|||
|
||||
|
||||
|
||||
### 10.8 Ten package managers on both editions (v0.4.4)
|
||||
|
||||
The package module runs the same on every distro it touches — module
|
||||
parity between the two frontends, not drift:
|
||||
|
||||
- **Cockpit edition** (`bridge/packages.py`): pacman (Arch) · emerge
|
||||
(Gentoo/Portage) · lunar (Lunar Linux) · sorcery (SourceMage) ·
|
||||
xbps (Void) · apk (Alpine) · zypper (openSUSE) · dnf / yum (RPM) ·
|
||||
apt (Debian). Detection is a `shutil.which` step-down in a fixed
|
||||
order, most specific first; `emerge` requires the `/var/db/pkg`
|
||||
corroboration; Void is probed via `xbps-query` (no bare `xbps`
|
||||
binary exists). Installs/removals/updates resolve from one
|
||||
`MUTATION_CMDS` table — `pacman -S --noconfirm`, `emerge
|
||||
--unmerge`, `cast`/`dispel`, `lin`/`lrm`, `xbps-install -y`,
|
||||
`zypper --non-interactive` — and still ride the cockpit superuser
|
||||
channel (polkit `org.sysdeck.packages.modify`).
|
||||
- **Web edition** (`web/src/lib/sysdeck/bridge/packages.ts`): the
|
||||
identical step-down and now the identical parser fixes — zypper
|
||||
tables parse by header-located columns, the emerge update preview
|
||||
anchors its capture after the class bracket (the old capture
|
||||
grabbed the bracket and silently dropped every row), and
|
||||
`xbps-query -Rs` rows parse with or without a repository prefix.
|
||||
- **Honest capability reporting**: lunar has no update-preview
|
||||
subcommand, so its summary says so instead of reporting 0 updates;
|
||||
lunar single-package update refuses with the real instruction.
|
||||
Fixture tests for all of the above run in `make check`
|
||||
(`tests/test_bridge_parsers.py::TestPackagesBackends`).
|
||||
- **BLOG.md** is now the long-form engineering essay (title, deck,
|
||||
decision-organized sections, canonical workflow, attribution
|
||||
footer). Release history lives in `QA.md` and `worklog.md`.
|
||||
|
||||
|
||||
## 11. Run without Cockpit (the complete standalone runbook, v0.3.0)
|
||||
|
||||
The web edition needs **nothing from sections 1–8** — no cockpit, no Python
|
||||
|
|
|
|||
54
README.md
54
README.md
|
|
@ -3,7 +3,7 @@
|
|||
**A drop-in plugin for an existing Cockpit install — twenty-six domain modules behind one dashboard.**
|
||||
|
||||
Author: **Jeremy Anderson** · <info@dcos.net> · <https://dcos.net>
|
||||
Version: **0.4.3** · License: **MIT**
|
||||
Version: **0.4.4** · License: **MIT**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -13,6 +13,44 @@ SysDeck is a cockpit-native plugin that consolidates the day-to-day work of a Li
|
|||
|
||||
The plugin ships as static HTML+JS+CSS plus a Python bridge helper package. It installs under `/usr/share/cockpit/sysdeck-*/` and is discovered automatically by the cockpit-bridge. No separate web server, no Node.js runtime, no database — the plugin runs inside the cockpit web service.
|
||||
|
||||
### v0.4.4 highlights (ten package managers on both editions + the blog essay)
|
||||
|
||||
The cockpit packages bridge now carries the **same ten-manager
|
||||
step-down as the web console** — module parity, not drift:
|
||||
|
||||
- **`bridge/packages.py` supports pacman (Arch), emerge (Gentoo/
|
||||
Portage), lunar (Lunar Linux), sorcery (SourceMage), xbps (Void),
|
||||
apk (Alpine), zypper (openSUSE), dnf and yum (RPM), apt (Debian)** —
|
||||
the identical detection order and corroboration rules as the web
|
||||
console's `packages.ts`: `emerge` claims the host only when
|
||||
`/var/db/pkg` also exists, and Void is probed through
|
||||
`xbps-query` because Void ships no bare `xbps` binary.
|
||||
- **Parser correctness, locked in by fixture tests** (`make check`):
|
||||
zypper tables parse by locating `Name`/`Current`/`Available`
|
||||
columns from the header row (zypper's leading status/repository
|
||||
columns vary by subcommand and release); the emerge update preview
|
||||
anchors its capture after the class bracket — portage pads the
|
||||
class field with spaces, and the previous capture grabbed the
|
||||
bracket itself and silently dropped every update row; `xbps-query
|
||||
-Rs` rows parse with or without a repository prefix. Both editions
|
||||
carry all three fixes.
|
||||
- **Honest capability reporting**: `lvu` has no update-preview
|
||||
subcommand, so the lunar backend returns an honest empty and the
|
||||
summary carries a note ("lunar has no update-preview subcommand —
|
||||
run lunar update to fetch + rebuild") instead of a zero count that
|
||||
reads as "all current"; lunar single-module update refuses with the
|
||||
real instruction. Every other manager maps to its exact argv
|
||||
(`emerge --unmerge`, `cast`/`dispel`, `zypper --non-interactive
|
||||
install`, …) from one `MUTATION_CMDS` table shared by install,
|
||||
remove, update, update-all, and the dry-run preview.
|
||||
- **`BLOG.md` is now a long-form engineering essay** (the same shape
|
||||
as the shellm blog: title, deck, decision-organized sections,
|
||||
canonical workflow, attribution footer) — a technical walkthrough
|
||||
of the auth model, the two-frontend architecture, the zero-demo
|
||||
contract, the ten-manager step-down, and the firewall privilege
|
||||
discipline, grounded in the source. Per-release history lives in
|
||||
`QA.md` and `worklog.md`.
|
||||
|
||||
### v0.4.3 highlights (the MoE QA pass — hardened on every axis)
|
||||
|
||||
A multi-expert review (design, CSS/UI-UX, JS/React/Next, Elm-style type
|
||||
|
|
@ -395,9 +433,9 @@ v0.0.36 adds a firewall backend dropdown to the Firewall panel and hardens the e
|
|||
|
||||
### v0.0.35 highlights
|
||||
|
||||
v0.0.35 restores SysDeck Kata as a standalone sidebar entry and adds three new modules per user directive — Jellyfin media server, photo manager, and remote filesystem manager:
|
||||
v0.0.35 promotes SysDeck Kata to a standalone sidebar entry and adds three new modules per user directive — Jellyfin media server, photo manager, and remote filesystem manager:
|
||||
|
||||
- **Kata split.** Per user directive: *"kata containers should be called SysDeck Kata and moved out of the tools area. and dont call it hidden thats akward."* The v0.0.34 layout had Kata Containers demoted to a hidden "tools" entry inside the merged Containers & VMs panel — labeled "Kata Containers (hidden helper)" with priority -1, in `plugins/sysdeck-containers-kata/`. v0.0.35 splits Kata back out: renamed to **SysDeck Kata**, moved to `plugins/sysdeck-kata/`, converted from a `tools` manifest entry to a `menu` entry (label "SysDeck Kata", order 27), removed the "hidden helper" wording, dropped the priority -1, and restored a dedicated keywords list. The Containers panel now manages Podman only — the Kata tab and its iframe were removed. The pre-built cockpit-kata React bundle (`index.js` + `index.css`) is shipped unchanged.
|
||||
- **Kata split.** Per user directive: *"kata containers should be called SysDeck Kata and moved out of the tools area. and dont call it hidden thats akward."* The v0.0.34 layout had Kata Containers demoted to a hidden "tools" entry inside the merged Containers & VMs panel — labeled "Kata Containers (hidden helper)" with priority -1, in `plugins/sysdeck-containers-kata/`. v0.0.35 splits Kata out: renamed to **SysDeck Kata**, moved to `plugins/sysdeck-kata/`, converted from a `tools` manifest entry to a `menu` entry (label "SysDeck Kata", order 27), removed the "hidden helper" wording, dropped the priority -1, and carries a dedicated keywords list. The Containers panel now manages Podman only — the Kata tab and its iframe were removed. The pre-built cockpit-kata React bundle (`index.js` + `index.css`) is shipped unchanged.
|
||||
- **Jellyfin media server module.** Per user directive: *"next we will integrate a jellyfin management module where it starts, stops, and loads the admin panel in the module."* New plugin `plugins/sysdeck-jellyfin/` + new bridge helper `bridge/jellyfin.py`. The bridge runs `systemctl start/stop/restart jellyfin.service` via the cockpit superuser channel (polkit `org.sysdeck.jellyfin.modify`); the panel iframes the running Jellyfin admin UI at `http://127.0.0.1:8096` — same pattern as the v0.0.34 Glances integration. Library list is best-effort via `GET /Library/VirtualFolders` on the local Jellyfin instance.
|
||||
- **Photo manager module.** Per user directive: *"as well as a photo manager of equal quality. with its own module."* New plugin `plugins/sysdeck-photos/` + new bridge helper `bridge/photos.py`. Multi-backend design (same shape as the DB Control module): PhotoPrism (port 2342, MIT), Piwigo (port 80, GPL-2.0), Lychee (port 80, MIT), Nextcloud-Memories (port 80, AGPL-3.0), LibrePhotos (port 3000, MIT). Each backend is auto-detected; the bridge runs `systemctl start/stop/restart <service>` and the panel iframes its admin UI when running. Polkit action: `org.sysdeck.photos.modify`.
|
||||
- **Remote FS manager module.** Per user directive: *"then a remote fs manager such as ceph, and others but not nfs or amanada fs."* New plugin `plugins/sysdeck-remotefs/` + new bridge helper `bridge/remotefs.py`. Multi-backend: Ceph (LGPL-2.1), GlusterFS (GPL-2.0), MooseFS (GPL-2.0), BeeGFS (BeeGFS EULA — free), OrangeFS (BSD-3). Each backend is auto-detected; the bridge runs `systemctl start/stop/restart <service>` and the cluster-info subcommand queries backend-specific cluster status (`ceph status --format=json`, `gluster pool list`, `moosefs-cli info`, `beegfs-ctl --listnodes`, `pvfs2-server -m`). Polkit action `org.sysdeck.remotefs.modify` authorizes the systemctl binary plus ceph / gluster / moosefs-cli / beegfs-ctl / pvfs2-server CLIs. **NFS and Amanda are explicitly EXCLUDED per directive** — documented in the panel footer and in `bridge/remotefs.py:EXCLUDED`.
|
||||
|
|
@ -465,7 +503,7 @@ The cockpit plugin is the primary deliverable.
|
|||
| 15 | System Monitor (Glances web UI) | `cockpit-glances` | P1 | `glances -w` (iframe) + snapshot cards |
|
||||
| 16 | Hardware Sensors | `cockpit-sensors` | P1 | `sensors` (lm_sensors) |
|
||||
| 17 | System Benchmark | `cockpit-benchmark` | P2 | `sysbench` |
|
||||
| 18 | Package Manager | `cockpit-packages` | P1 | `pacman` / `dnf` / `apt` |
|
||||
| 18 | Package Manager | `cockpit-packages` | P1 | ten managers: `pacman` / `emerge` / `lunar` / `sorcery` / `xbps` / `apk` / `zypper` / `dnf` / `yum` / `apt` |
|
||||
| 19 | Policy & Permissions | `cockpit-policy` | P1 | ACLs · cgroups v2 · VLANs · eBPF · namespaces · filecaps · LSM stack (AppArmor/Smack/TOMOYO/Yama/LoadPin/Lockdown/BPF-LSM/Landlock) |
|
||||
| 20 | DB Control | `cockpit-db` | P1 | DB engine CLIs (SQL/NoSQL/vector/AI) |
|
||||
| 21 | Jellyfin Media Server | `cockpit-jellyfin` | P1 | `systemctl start/stop/restart jellyfin.service` + admin UI iframe (port 8096) |
|
||||
|
|
@ -490,7 +528,7 @@ sysdeck-0.0.35/
|
|||
│ ├── sysdeck-mesh/
|
||||
│ ├── sysdeck-vault/
|
||||
│ ├── sysdeck-fleet/
|
||||
│ ├── sysdeck-kata/ # v0.0.35: restored to standalone sidebar entry — pre-built cockpit-kata React app
|
||||
│ ├── sysdeck-kata/ # v0.0.35: standalone sidebar entry — pre-built cockpit-kata React app
|
||||
│ ├── sysdeck-fester/
|
||||
│ ├── sysdeck-firmware/
|
||||
│ ├── sysdeck-builder/
|
||||
|
|
@ -519,7 +557,7 @@ sysdeck-0.0.35/
|
|||
│ ├── glances.py # v0.0.34: snapshot + start-web/stop-web
|
||||
│ ├── sensors.py # lm_sensors normalization + alert thresholds
|
||||
│ ├── benchmark.py # sysbench result parsing + baselines
|
||||
│ ├── packages.py # pacman/dnf/apt unified package ops
|
||||
│ ├── packages.py # ten-manager unified package ops (pacman→apt step-down)
|
||||
│ ├── mining.py # v0.0.34: XMRig REST API power tool
|
||||
│ ├── themes.py # v0.0.34: cockpit.conf + CSS variable surface
|
||||
│ ├── policy.py # Policy & Permissions module (LSM stack)
|
||||
|
|
@ -543,7 +581,7 @@ sysdeck-0.0.35/
|
|||
├── docs/ # INSTALL.md
|
||||
├── README.md
|
||||
├── QUICKSTART.md
|
||||
├── BLOG.md # release narrative
|
||||
├── BLOG.md # engineering essay (long-form)
|
||||
├── QA.md # QA notes per release
|
||||
├── THIRD_PARTY.md # third-party attributions
|
||||
├── LICENSE # MIT
|
||||
|
|
@ -616,7 +654,7 @@ MIT — see [LICENSE](./LICENSE). Third-party attributions: see [THIRD_PARTY.md]
|
|||
|
||||
## Release notes
|
||||
|
||||
See [BLOG.md](./BLOG.md) for the v0.0.33 release narrative and prior-version history.
|
||||
See [BLOG.md](./BLOG.md) for the engineering essay — a long-form technical walkthrough of the architecture and design decisions, written against the current release. Per-version history lives in [worklog.md](./worklog.md) and [QA.md](./QA.md).
|
||||
|
||||
## Project history
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import os
|
|||
import subprocess
|
||||
from typing import Literal
|
||||
|
||||
__version__ = "0.4.3"
|
||||
__version__ = "0.4.4"
|
||||
__author__ = "Jeremy Anderson"
|
||||
__url__ = "https://dcos.net"
|
||||
|
||||
|
|
@ -33,7 +33,9 @@ __url__ = "https://dcos.net"
|
|||
# then fall back to checking which package manager is available.
|
||||
# Returns a normalized distro identifier for use in dispatch tables.
|
||||
|
||||
DistroId = Literal["arch", "debian", "fedora", "rhel", "unknown"]
|
||||
DistroId = Literal["arch", "gentoo", "lunar", "sourcemage", "void",
|
||||
"alpine", "opensuse", "debian", "fedora", "rhel",
|
||||
"unknown"]
|
||||
|
||||
|
||||
def detect_distro() -> DistroId:
|
||||
|
|
@ -42,9 +44,12 @@ def detect_distro() -> DistroId:
|
|||
Priority order:
|
||||
1. Parse /etc/os-release ID/ID_LIKE fields.
|
||||
2. Fall back to package-manager presence (pacman → arch,
|
||||
emerge → gentoo, lunar → lunar, sorcery → sourcemage,
|
||||
xbps-query → void, apk → alpine, zypper → opensuse,
|
||||
apt → debian, dnf → fedora).
|
||||
|
||||
Returns one of: 'arch', 'debian', 'fedora', 'rhel', 'unknown'.
|
||||
Returns one of: 'arch', 'gentoo', 'lunar', 'sourcemage', 'void',
|
||||
'alpine', 'opensuse', 'debian', 'fedora', 'rhel', 'unknown'.
|
||||
"""
|
||||
# Try /etc/os-release first (present on all modern distros).
|
||||
try:
|
||||
|
|
@ -59,6 +64,12 @@ def detect_distro() -> DistroId:
|
|||
|
||||
# Direct match on ID.
|
||||
id_map = {"arch": "arch", "archlinux": "arch",
|
||||
"gentoo": "gentoo", "funtoo": "gentoo",
|
||||
"lunar": "lunar", "sourcemage": "sourcemage",
|
||||
"void": "void",
|
||||
"alpine": "alpine", "postmarketos": "alpine",
|
||||
"opensuse": "opensuse", "opensuse-leap": "opensuse",
|
||||
"opensuse-tumbleweed": "opensuse", "sles": "opensuse",
|
||||
"debian": "debian", "ubuntu": "debian", "linuxmint": "debian", "pop": "debian",
|
||||
"fedora": "fedora", "rhel": "rhel", "centos": "rhel", "rocky": "rhel", "alma": "rhel"}
|
||||
if dist_id in id_map:
|
||||
|
|
@ -72,7 +83,11 @@ def detect_distro() -> DistroId:
|
|||
pass
|
||||
|
||||
# Fall back to package manager presence.
|
||||
for cmd, distro in [("pacman", "arch"), ("apt", "debian"), ("dnf", "fedora")]:
|
||||
for cmd, distro in [("pacman", "arch"), ("emerge", "gentoo"),
|
||||
("lunar", "lunar"), ("sorcery", "sourcemage"),
|
||||
("xbps-query", "void"), ("apk", "alpine"),
|
||||
("zypper", "opensuse"),
|
||||
("apt", "debian"), ("dnf", "fedora")]:
|
||||
try:
|
||||
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
||||
return distro
|
||||
|
|
@ -88,16 +103,25 @@ DISTRO: DistroId = detect_distro()
|
|||
|
||||
# ── Package manager detection ───────────────────────────────────────
|
||||
#
|
||||
# Returns the command name for the system's package manager.
|
||||
# Arch → pacman, Debian → apt, Fedora/RHEL → dnf.
|
||||
# Returns the manager id for the system's distro, matching the ten
|
||||
# backends bridge/packages.py steps down through: Arch → pacman,
|
||||
# Gentoo → emerge, Lunar → lunar, SourceMage → sorcery, Void → xbps,
|
||||
# Alpine → apk, openSUSE → zypper, Fedora/RHEL → dnf, Debian → apt.
|
||||
|
||||
PkgManager = Literal["pacman", "apt", "dnf", "unknown"]
|
||||
PkgManager = Literal["pacman", "emerge", "lunar", "sorcery", "xbps",
|
||||
"apk", "zypper", "dnf", "apt", "unknown"]
|
||||
|
||||
|
||||
def detect_pkg_manager() -> PkgManager:
|
||||
"""Detect the system package manager based on distro."""
|
||||
pkg_map: dict[DistroId, PkgManager] = {
|
||||
"arch": "pacman",
|
||||
"gentoo": "emerge",
|
||||
"lunar": "lunar",
|
||||
"sourcemage": "sorcery",
|
||||
"void": "xbps",
|
||||
"alpine": "apk",
|
||||
"opensuse": "zypper",
|
||||
"debian": "apt",
|
||||
"fedora": "dnf",
|
||||
"rhel": "dnf",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"_comment": "Compatibility Manifest — sysdeck v0.1.3",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"suite_requires": { "cockpit": ">=239", "python": ">=3.9" },
|
||||
"modules": {
|
||||
"containers": {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# Upstream: https://dcos.net
|
||||
|
||||
pkgname=sysdeck
|
||||
pkgver=0.4.3
|
||||
pkgver=0.4.4
|
||||
pkgrel=1
|
||||
pkgdesc="Unified operations surface for Linux infrastructure — twenty-six domain modules behind one Cockpit dashboard"
|
||||
arch=('any')
|
||||
|
|
|
|||
|
|
@ -1,3 +1,20 @@
|
|||
sysdeck (0.4.4-1) unstable; urgency=medium
|
||||
|
||||
* v0.4.4: ten package-manager backends on both editions + the blog
|
||||
essay. The cockpit packages bridge (bridge/packages.py) carries
|
||||
the same ten-manager step-down as the web console — pacman,
|
||||
emerge (corroborated by /var/db/pkg), lunar, sorcery, xbps
|
||||
(probed via xbps-query), apk, zypper, dnf, yum, apt — with a
|
||||
unified MUTATION_CMDS table, header-located zypper table parsing,
|
||||
an emerge update regex anchored after the class bracket (the old
|
||||
capture grabbed the bracket and dropped every row), and honest
|
||||
capability reporting for managers without an update preview
|
||||
(lunar). The web console gains the same parser fixes and the
|
||||
xbps-query detection probe. BLOG.md is now a single long-form
|
||||
technical essay (the shellm blog pattern), not release notes.
|
||||
|
||||
-- Jeremy Anderson <info@dcos.net> Sat, 12 Sep 2026 18:00:00 -0400
|
||||
|
||||
sysdeck (0.4.3-1) unstable; urgency=medium
|
||||
|
||||
* v0.4.3: the MoE QA pass — production hardening across every axis.
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@
|
|||
</action>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- Package management: pacman, apt, dnf install/remove/upgrade. -->
|
||||
<!-- Package management: pacman, emerge, lunar, sorcery, xbps, -->
|
||||
<!-- apk, zypper, dnf/yum, apt install/remove/upgrade. -->
|
||||
<!-- Read-only operations (list, search, info) do not need this. -->
|
||||
<!-- ============================================================= -->
|
||||
<action id="org.sysdeck.packages.modify">
|
||||
|
|
@ -105,6 +106,12 @@
|
|||
<allow_active>auth_admin_keep</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/pacman</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/emerge</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/lunar</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/sorcery</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/xbps-install</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/sbin/apk</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/zypper</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/apt</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/dnf</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/yum</annotate>
|
||||
|
|
@ -154,8 +161,8 @@
|
|||
<!-- /var/lib/sysdeck/builder/. The bridge runs the backend via -->
|
||||
<!-- subprocess; the cockpit superuser channel handles root priv. -->
|
||||
<!-- v0.1.0: profile-import-packages also queries the host's -->
|
||||
<!-- package manager (pacman -Qqe / apt-mark showmanual / -->
|
||||
<!-- dnf repoquery --userinstalled) to capture the operator's -->
|
||||
<!-- package manager (pacman -Qqe / apt-mark showmanual / the dnf -->
|
||||
<!-- repoquery userinstalled filter) to capture the operator's -->
|
||||
<!-- explicitly-installed package set. -->
|
||||
<!-- ============================================================= -->
|
||||
<action id="org.sysdeck.builder.modify">
|
||||
|
|
@ -176,6 +183,12 @@
|
|||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/pacman</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/apt-mark</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/dnf</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/emerge</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/lvu</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/gaze</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/xbps-query</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/sbin/apk</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/zypper</annotate>
|
||||
</action>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import shutil
|
|||
import subprocess
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
VERSION = "0.4.3"
|
||||
VERSION = "0.4.4"
|
||||
PACKAGE = "sysdeck"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
# Debian/Ubuntu users: see packaging/debian/
|
||||
|
||||
Name: sysdeck
|
||||
Version: 0.4.3
|
||||
Version: 0.4.4
|
||||
Release: 1%{?dist}
|
||||
Summary: Unified operations surface for Linux infrastructure
|
||||
|
||||
|
|
@ -84,6 +84,16 @@ if [ $1 -eq 0 ]; then
|
|||
fi
|
||||
|
||||
%changelog
|
||||
* Sat Sep 12 2026 Jeremy Anderson <info@dcos.net> - 0.4.4-1
|
||||
- v0.4.4: package parity + blog essay. Ten package-manager backends on
|
||||
both editions (bridge/packages.py gains emerge/lunar/sorcery/xbps/
|
||||
apk/zypper/yum with a unified MUTATION_CMDS table and shutil.which
|
||||
detection step-down); zypper tables parse by header-located columns;
|
||||
the emerge update regex anchors after the class bracket; xbps
|
||||
detection probes xbps-query; lunar reports its missing update-preview
|
||||
honestly. Web packages.ts gains the same fixes. BLOG.md becomes the
|
||||
long-form engineering essay.
|
||||
|
||||
* Sat Sep 12 2026 Jeremy Anderson <info@dcos.net> - 0.4.3-1
|
||||
- v0.4.3: the MoE QA pass — production hardening across every axis.
|
||||
Privileged writes ride stdin (polkit rules verified post-write,
|
||||
|
|
|
|||
|
|
@ -1,32 +1,28 @@
|
|||
/*
|
||||
* SysDeck - Packages Panel (v0.0.31)
|
||||
* SysDeck - Packages Panel
|
||||
* Author: Jeremy Anderson (https://dcos.net)
|
||||
*
|
||||
* Package management panel — list, search, install, update, and remove
|
||||
* packages via the system package manager (pacman / dnf / apt).
|
||||
* packages via the system package manager. Ten managers, the same
|
||||
* step-down as the bridge: pacman (Arch) · emerge (Gentoo) · lunar
|
||||
* (Lunar) · sorcery (SourceMage) · xbps (Void) · apk (Alpine) ·
|
||||
* zypper (openSUSE) · dnf / yum (RPM) · apt (Debian).
|
||||
* The package manager is invoked as a separate process via cockpit.spawn —
|
||||
* no package-manager code is bundled.
|
||||
*
|
||||
* v0.0.31 REWRITE — UPDATE NEEDS SUDO, FIXED THE COCKPIT WAY.
|
||||
* v0.0.30 packages.js Update All button called bridge.packages.updateAll()
|
||||
* which returned only the command string that *would* be run. The panel
|
||||
* showed `alert("Run this command with superuser privileges.")` and the
|
||||
* operator had to copy the command, open a terminal, sudo, paste, run.
|
||||
* That defeated the purpose of having a panel.
|
||||
* Mutations (install/remove/update/update-all) execute via the cockpit
|
||||
* superuser channel (polkit): the bridge helper runs the detected
|
||||
* package manager via subprocess, and this panel subscribes to the
|
||||
* cockpit spawn stream so the operator sees live stdout/stderr in a
|
||||
* <pre> log panel — exactly like cockpit's own Packages and Software
|
||||
* Updates panels. No `sudo` shell-out from JS.
|
||||
*
|
||||
* v0.0.31 makes install/remove/update/update-all actually execute via
|
||||
* the cockpit superuser channel (polkit). The bridge helper runs the
|
||||
* detected package manager via subprocess, and the JS panel subscribes
|
||||
* to the cockpit spawn stream so the operator sees live stdout/stderr
|
||||
* in a <pre> log panel — exactly like cockpit's own Packages and
|
||||
* Software Updates panels. No `sudo` shell-out from JS.
|
||||
*
|
||||
* v0.1.4 SECURITY: every dynamic string interpolated into innerHTML
|
||||
* SECURITY: every dynamic string interpolated into innerHTML
|
||||
* (package names, versions, descriptions, search terms echoed back,
|
||||
* error messages) now goes through escapeHtml(). Package metadata is
|
||||
* live data from pacman/dnf/apt output — a typo-squat repo or a
|
||||
* locally-installed package whose name/description contains markup
|
||||
* used to execute in the cockpit admin session (0.3.0 audit).
|
||||
* error messages) goes through escapeHtml(). Package metadata is
|
||||
* live data from the package manager's output — a typo-squat repo or
|
||||
* a locally-installed package whose name/description contains markup
|
||||
* must not execute in the cockpit admin session (0.3.0 audit).
|
||||
* Raw tool output already flows through textContent (showOutput),
|
||||
* which is safe.
|
||||
*/
|
||||
|
|
@ -59,11 +55,16 @@ export async function mount(panel, { bridge, EventBus }) {
|
|||
const instCount = summary.installedCount || installed.length;
|
||||
const updCount = summary.updateCount || 0;
|
||||
const updates = summary.updates || [];
|
||||
// Managers without an update-preview subcommand (lunar) carry the
|
||||
// explanation instead of a count that would read as "all current".
|
||||
const updLine = summary.updatesNote
|
||||
? `${escapeHtml(mgr)} — ${instCount} installed · ${escapeHtml(summary.updatesNote)}`
|
||||
: `${escapeHtml(mgr)} — ${instCount} installed · ${updCount} updates available`;
|
||||
|
||||
panel.innerHTML = `
|
||||
<header>
|
||||
<h2 class="suite-panel-title">Package Manager</h2>
|
||||
<p class="suite-panel-subtitle">${escapeHtml(mgr)} — ${instCount} installed · ${updCount} updates available</p>
|
||||
<p class="suite-panel-subtitle">${updLine}</p>
|
||||
</header>
|
||||
<div class="suite-row">
|
||||
<div class="suite-card suite-col-2">
|
||||
|
|
@ -220,6 +221,6 @@ function renderSkeleton() {
|
|||
function renderError(err) {
|
||||
return `<div class="suite-card">
|
||||
<h3 class="suite-card-title">Package manager unavailable</h3>
|
||||
<p class="suite-card-body suite-muted">${escapeHtml(err.message || err)}. Ensure pacman, dnf, or apt is installed.</p>
|
||||
<p class="suite-card-body suite-muted">${escapeHtml(err.message || err)}. Ensure a supported package manager is installed (pacman, emerge, lunar, sorcery, xbps, apk, zypper, dnf, yum, or apt).</p>
|
||||
</div>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Standalone fixture tests for the ten-backend packages bridge parsers.
|
||||
|
||||
Runs outside the unittest suite for quick iteration; the same fixtures
|
||||
live in tests/test_bridge_parsers.py (TestPackagesBackends)."""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "bridge"))
|
||||
import packages as p # noqa: E402
|
||||
|
||||
|
||||
def check_zypper_table():
|
||||
layout_a = (
|
||||
"S | Repository | Name | Current | Available | Arch\n"
|
||||
"--+------------------+------------+---------+-----------+-------\n"
|
||||
"v | openSUSE-OSS | glib2 | 2.78 | 2.80 | x86_64\n"
|
||||
" | openSUSE-OSS | zypper | 1.14.70 | 1.14.74 | x86_64"
|
||||
)
|
||||
h, r = p._zypper_table(layout_a, ("Name", "Current", "Available"))
|
||||
assert h == {"Name": 2, "Current": 3, "Available": 4}, h
|
||||
assert len(r) == 2
|
||||
print("zypper layout A (status+repo prefix) OK")
|
||||
|
||||
layout_b = (
|
||||
"S# | Repository | Name | Current | Available | Arch\n"
|
||||
"---+------------------+------------+---------+-----------+-------\n"
|
||||
" 1 | openSUSE-OSS | glib2 | 2.78 | 2.80 | x86_64"
|
||||
)
|
||||
h, r = p._zypper_table(layout_b, ("Name", "Current", "Available"))
|
||||
assert r and r[0][2] == "glib2"
|
||||
print("zypper layout B (numbered prefix) OK")
|
||||
|
||||
se = (
|
||||
"S | Name | Summary | Type\n"
|
||||
"--+-------------+----------------------------+-----------\n"
|
||||
" | packagekit | PackageKit service | package\n"
|
||||
"i | glib2 | GLib library | package"
|
||||
)
|
||||
h, r = p._zypper_table(se, ("Name", "Summary"))
|
||||
assert h == {"Name": 1, "Summary": 2}, h
|
||||
assert r[1][0] == "i"
|
||||
print("zypper se table OK")
|
||||
|
||||
noisy = (
|
||||
"Name | Current | Available\n"
|
||||
"-----+---------+----------\n"
|
||||
"glib2 | 2.78 | 2.80\n"
|
||||
"Name | Current | Available\n"
|
||||
"bash | 5.2 | 5.3"
|
||||
)
|
||||
h, r = p._zypper_table(noisy, ("Name", "Current", "Available"))
|
||||
assert len(r) == 2 and r[1][0] == "bash", r
|
||||
print("separator + repeat-header filtering OK")
|
||||
|
||||
|
||||
def check_emerge_regex():
|
||||
rx = r"\[ebuild\s+U[^\]]*\]\s*(\S+)(?:\s+\[([^\]]+)\])?"
|
||||
m = re.search(rx, " [ebuild U ] dev-lang/python-3.12.4 [3.12.3]")
|
||||
assert m and m.group(1) == "dev-lang/python-3.12.4" and m.group(2) == "3.12.3", m.groups()
|
||||
assert re.search(rx, " [ebuild NS ] dev-lang/python-3.11.8 [3.11.6]") is None
|
||||
assert re.search(rx, " [ebuild N ] app-misc/newpkg-1.0") is None
|
||||
m = re.search(rx, " [ebuild U r ] sys-libs/glibc-2.38-r9 [2.37-r7]")
|
||||
assert m and m.group(1) == "sys-libs/glibc-2.38-r9" and m.group(2) == "2.37-r7", m.groups()
|
||||
print("emerge update regex OK (padded class field; N/NS rows excluded)")
|
||||
|
||||
|
||||
def check_xbps_regexes():
|
||||
m = re.match(r"^ii\s+(\S+)\s+(.*)$", "ii firefox-128.0_1 The Firefox web browser")
|
||||
assert m and m.group(1) == "firefox-128.0_1"
|
||||
# -Rs rows may or may not carry a repository prefix.
|
||||
rx = r"^\[\*\]\s+(?:\S+/)?(\S+)\s+-\s+(.*)$"
|
||||
m = re.match(rx, "[*] firefox-128.0_1 - The Firefox web browser")
|
||||
assert m and m.group(1) == "firefox-128.0_1" and m.group(2) == "The Firefox web browser"
|
||||
m = re.match(rx, "[*] void-repo/firefox-128.0_1 - The Firefox web browser")
|
||||
assert m and m.group(1) == "firefox-128.0_1", m.groups()
|
||||
print("xbps regexes OK")
|
||||
|
||||
|
||||
def check_helpers():
|
||||
info = p._parse_colon_blocks("Name : curl\nVersion : 8.6.0\nInstalled Size: 1.2 MiB")
|
||||
assert info["name"] == "curl" and info["version"] == "8.6.0"
|
||||
assert info["installed_size"] == "1.2 MiB"
|
||||
assert p._split_name_ver("gcc-13.2.1-r0") == ("gcc", "13.2.1-r0")
|
||||
assert p._split_name_ver("linux-headers-6.1") == ("linux-headers", "6.1")
|
||||
assert p._split_name_ver("firefox-128.0_1") == ("firefox", "128.0_1")
|
||||
assert p._split_name_ver("bash") == ("bash", "")
|
||||
print("colon blocks + split_name_ver OK")
|
||||
|
||||
|
||||
def check_fallbacks():
|
||||
orig = p.run
|
||||
p.run = lambda argv, timeout=60, ok_rcs=(): ""
|
||||
assert p._lunar_list_installed() == []
|
||||
assert p._sorcery_list_installed() == []
|
||||
assert p._lunar_list_updates() == []
|
||||
p.run = orig
|
||||
print("lunar/sorcery honest-empty fallbacks OK")
|
||||
|
||||
|
||||
def check_mutation_table():
|
||||
for mgr in ("pacman", "emerge", "lunar", "sorcery", "xbps", "apk",
|
||||
"zypper", "dnf", "yum", "apt"):
|
||||
p.PKG_MANAGER = mgr
|
||||
assert p._mutation_cmd("install", "x"), mgr
|
||||
assert p._mutation_cmd("remove", "x"), mgr
|
||||
assert p._mutation_cmd("update-all", ""), mgr
|
||||
if mgr != "lunar":
|
||||
assert p._mutation_cmd("update", "x"), mgr
|
||||
else:
|
||||
assert p._mutation_cmd("update", "x") == [], mgr
|
||||
p.PKG_MANAGER = "unknown"
|
||||
assert p._mutation_cmd("install", "x") == []
|
||||
p.PKG_MANAGER = p._detect_pkg_manager()
|
||||
print("mutation table covers all ten managers (lunar update honestly absent) OK")
|
||||
|
||||
|
||||
def check_detection_probes():
|
||||
ids = [m for m, _ in p.DETECT_PROBES]
|
||||
assert ids == ["pacman", "emerge", "lunar", "sorcery", "xbps",
|
||||
"apk", "zypper", "dnf", "yum", "apt"], ids
|
||||
# xbps must probe xbps-query: Void ships no bare `xbps` binary.
|
||||
assert dict(p.DETECT_PROBES)["xbps"] == "xbps-query"
|
||||
print("detection probe order + xbps-query probe OK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_zypper_table()
|
||||
check_emerge_regex()
|
||||
check_xbps_regexes()
|
||||
check_helpers()
|
||||
check_fallbacks()
|
||||
check_mutation_table()
|
||||
check_detection_probes()
|
||||
print("ALL PACKAGES-BACKEND CHECKS OK")
|
||||
|
|
@ -1914,8 +1914,8 @@ class TestFirewallV047ManifestsAndMetainfo(unittest.TestCase):
|
|||
)
|
||||
|
||||
def test_version_sync_all_surfaces_report_020(self):
|
||||
# Every release surface must report v0.4.3 (MoE QA production pass).
|
||||
v = "0.4.3"
|
||||
# Every release surface must report v0.4.4 (package parity + blog essay).
|
||||
v = "0.4.4"
|
||||
files_to_check = [
|
||||
"Makefile",
|
||||
"bridge/__init__.py",
|
||||
|
|
@ -1934,6 +1934,177 @@ class TestFirewallV047ManifestsAndMetainfo(unittest.TestCase):
|
|||
f"{rel} does not reference version {v}")
|
||||
|
||||
|
||||
class TestPackagesBackends(unittest.TestCase):
|
||||
"""v0.4.4: the ten-backend packages bridge.
|
||||
|
||||
Fixture tests for the detection step-down, the shared parsers, and
|
||||
the mutation command table — the same guarantees the web console's
|
||||
packages.ts carries, locked in on the cockpit side."""
|
||||
|
||||
def setUp(self):
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "bridge"))
|
||||
import packages
|
||||
self.packages = packages
|
||||
self._orig_manager = packages.PKG_MANAGER
|
||||
self._orig_run = packages.run
|
||||
|
||||
def tearDown(self):
|
||||
self.packages.PKG_MANAGER = self._orig_manager
|
||||
self.packages.run = self._orig_run
|
||||
|
||||
def test_detection_probe_order_and_xbps_probe(self):
|
||||
ids = [m for m, _ in self.packages.DETECT_PROBES]
|
||||
self.assertEqual(
|
||||
ids,
|
||||
["pacman", "emerge", "lunar", "sorcery", "xbps",
|
||||
"apk", "zypper", "dnf", "yum", "apt"],
|
||||
)
|
||||
# Void ships no bare `xbps` binary — xbps-query is the probe.
|
||||
self.assertEqual(dict(self.packages.DETECT_PROBES)["xbps"], "xbps-query")
|
||||
|
||||
def test_detection_requires_emerge_corroboration(self):
|
||||
import shutil
|
||||
import unittest.mock as mock
|
||||
fake_which = lambda b: "/usr/bin/emerge" if b == "emerge" else None
|
||||
# emerge present but no /var/db/pkg → step down past it.
|
||||
with mock.patch.object(shutil, "which", side_effect=fake_which), \
|
||||
mock.patch.object(self.packages.os.path, "isdir", return_value=False):
|
||||
self.assertNotEqual(self.packages._detect_pkg_manager(), "emerge")
|
||||
with mock.patch.object(shutil, "which", side_effect=fake_which), \
|
||||
mock.patch.object(self.packages.os.path, "isdir", return_value=True):
|
||||
self.assertEqual(self.packages._detect_pkg_manager(), "emerge")
|
||||
|
||||
def test_detection_unknown_when_nothing_installed(self):
|
||||
import shutil
|
||||
import unittest.mock as mock
|
||||
with mock.patch.object(shutil, "which", return_value=None):
|
||||
self.assertEqual(self.packages._detect_pkg_manager(), "unknown")
|
||||
|
||||
def test_split_name_ver(self):
|
||||
sv = self.packages._split_name_ver
|
||||
self.assertEqual(sv("gcc-13.2.1-r0"), ("gcc", "13.2.1-r0"))
|
||||
self.assertEqual(sv("linux-headers-6.1"), ("linux-headers", "6.1"))
|
||||
self.assertEqual(sv("firefox-128.0_1"), ("firefox", "128.0_1"))
|
||||
self.assertEqual(sv("bash"), ("bash", ""))
|
||||
|
||||
def test_parse_colon_blocks(self):
|
||||
info = self.packages._parse_colon_blocks(
|
||||
"Name : curl\nVersion : 8.6.0\nInstalled Size: 1.2 MiB"
|
||||
)
|
||||
self.assertEqual(info["name"], "curl")
|
||||
self.assertEqual(info["version"], "8.6.0")
|
||||
self.assertEqual(info["installed_size"], "1.2 MiB")
|
||||
|
||||
def test_zypper_table_locates_columns_from_header(self):
|
||||
zt = self.packages._zypper_table
|
||||
# Layout with status + repository prefix columns.
|
||||
layout_a = (
|
||||
"S | Repository | Name | Current | Available | Arch\n"
|
||||
"--+------------------+------------+---------+-----------+-------\n"
|
||||
"v | openSUSE-OSS | glib2 | 2.78 | 2.80 | x86_64\n"
|
||||
" | openSUSE-OSS | zypper | 1.14.70 | 1.14.74 | x86_64"
|
||||
)
|
||||
h, rows = zt(layout_a, ("Name", "Current", "Available"))
|
||||
self.assertEqual(h, {"Name": 2, "Current": 3, "Available": 4})
|
||||
self.assertEqual(len(rows), 2)
|
||||
self.assertEqual(rows[0][2], "glib2")
|
||||
# Layout with a numbered prefix column.
|
||||
layout_b = (
|
||||
"S# | Repository | Name | Current | Available | Arch\n"
|
||||
"---+------------------+------------+---------+-----------+-------\n"
|
||||
" 1 | openSUSE-OSS | glib2 | 2.78 | 2.80 | x86_64"
|
||||
)
|
||||
h, rows = zt(layout_b, ("Name", "Current", "Available"))
|
||||
self.assertEqual(rows[0][2], "glib2")
|
||||
# Separator rows and repeated headers are filtered.
|
||||
noisy = (
|
||||
"Name | Current | Available\n"
|
||||
"-----+---------+----------\n"
|
||||
"glib2 | 2.78 | 2.80\n"
|
||||
"Name | Current | Available\n"
|
||||
"bash | 5.2 | 5.3"
|
||||
)
|
||||
h, rows = zt(noisy, ("Name", "Current", "Available"))
|
||||
self.assertEqual(len(rows), 2)
|
||||
self.assertEqual(rows[1][0], "bash")
|
||||
|
||||
def test_emerge_update_regex_anchors_after_bracket(self):
|
||||
import re
|
||||
rx = r"\[ebuild\s+U[^\]]*\]\s*(\S+)(?:\s+\[([^\]]+)\])?"
|
||||
# Portage pads the class field with spaces before the bracket.
|
||||
m = re.search(rx, " [ebuild U ] dev-lang/python-3.12.4 [3.12.3]")
|
||||
self.assertEqual(m.group(1), "dev-lang/python-3.12.4")
|
||||
self.assertEqual(m.group(2), "3.12.3")
|
||||
m = re.search(rx, " [ebuild U r ] sys-libs/glibc-2.38-r9 [2.37-r7]")
|
||||
self.assertEqual(m.group(1), "sys-libs/glibc-2.38-r9")
|
||||
# N (new) and NS (new slot) rows are not updates.
|
||||
self.assertIsNone(re.search(rx, " [ebuild N ] app-misc/newpkg-1.0"))
|
||||
self.assertIsNone(re.search(rx, " [ebuild NS ] dev-lang/python-3.11.8 [3.11.6]"))
|
||||
|
||||
def test_xbps_search_regex_tolerates_missing_repo_prefix(self):
|
||||
import re
|
||||
rx = r"^\[\*\]\s+(?:\S+/)?(\S+)\s+-\s+(.*)$"
|
||||
m = re.match(rx, "[*] firefox-128.0_1 - The Firefox web browser")
|
||||
self.assertEqual(m.group(1), "firefox-128.0_1")
|
||||
m = re.match(rx, "[*] void-repo/firefox-128.0_1 - The Firefox web browser")
|
||||
self.assertEqual(m.group(1), "firefox-128.0_1")
|
||||
|
||||
def test_mutation_table_covers_all_ten_managers(self):
|
||||
pkg = self.packages
|
||||
for mgr in ("pacman", "emerge", "lunar", "sorcery", "xbps",
|
||||
"apk", "zypper", "dnf", "yum", "apt"):
|
||||
pkg.PKG_MANAGER = mgr
|
||||
self.assertTrue(pkg._mutation_cmd("install", "x"), mgr)
|
||||
self.assertTrue(pkg._mutation_cmd("remove", "x"), mgr)
|
||||
self.assertTrue(pkg._mutation_cmd("update-all", ""), mgr)
|
||||
if mgr == "lunar":
|
||||
# Lunar has no single-module update — the honest absence.
|
||||
self.assertEqual(pkg._mutation_cmd("update", "x"), [])
|
||||
else:
|
||||
self.assertTrue(pkg._mutation_cmd("update", "x"), mgr)
|
||||
pkg.PKG_MANAGER = "unknown"
|
||||
self.assertEqual(pkg._mutation_cmd("install", "x"), [])
|
||||
|
||||
def test_mutation_commands_are_real_manager_invocations(self):
|
||||
pkg = self.packages
|
||||
pkg.PKG_MANAGER = "emerge"
|
||||
self.assertEqual(pkg._mutation_cmd("remove", "foo"),
|
||||
["emerge", "--unmerge", "foo"])
|
||||
self.assertEqual(pkg._mutation_cmd("update-all", ""),
|
||||
["emerge", "-u", "-D", "@world"])
|
||||
pkg.PKG_MANAGER = "zypper"
|
||||
self.assertEqual(pkg._mutation_cmd("install", "foo"),
|
||||
["zypper", "--non-interactive", "install", "foo"])
|
||||
pkg.PKG_MANAGER = "sorcery"
|
||||
self.assertEqual(pkg._mutation_cmd("install", "foo"), ["cast", "foo"])
|
||||
self.assertEqual(pkg._mutation_cmd("remove", "foo"), ["dispel", "foo"])
|
||||
|
||||
def test_lunar_reports_honest_empty_updates(self):
|
||||
# run() returns '' on real failure — lunar has no preview
|
||||
# subcommand, so the list is empty and the summary carries the
|
||||
# note explaining why.
|
||||
self.packages.run = lambda argv, timeout=60, ok_rcs=(): ""
|
||||
self.assertEqual(self.packages._lunar_list_updates(), [])
|
||||
self.packages.PKG_MANAGER = "lunar"
|
||||
summary = self.packages.summary()
|
||||
self.assertEqual(summary["updateCount"], 0)
|
||||
self.assertIn("update-preview", summary["updatesNote"])
|
||||
|
||||
def test_read_backend_dispatch_has_all_ten(self):
|
||||
pkg = self.packages
|
||||
for mgr in ("pacman", "emerge", "lunar", "sorcery", "xbps",
|
||||
"apk", "zypper", "dnf", "yum", "apt"):
|
||||
backend = pkg._backend(mgr)
|
||||
for cmd in ("list-installed", "list-updates", "search", "info"):
|
||||
self.assertIn(cmd, backend, f"{mgr} missing {cmd}")
|
||||
self.assertEqual(pkg._backend("unknown"), {})
|
||||
|
||||
def test_no_sudo_shell_out_in_packages_bridge(self):
|
||||
import inspect
|
||||
source = inspect.getsource(self.packages)
|
||||
self.assertNotIn('"/usr/bin/sudo"', source)
|
||||
|
||||
|
||||
class TestBuilderProfileCopy(unittest.TestCase):
|
||||
"""v0.0.48: unit tests for bridge.builder.profile_copy().
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "nextjs_tailwind_shadcn_ts",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -H 127.0.0.1 -p 3000 2>&1 | tee dev.log",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
#
|
||||
# Preconditions:
|
||||
# - master-build/cockpit/ holds the staged + upgraded cockpit tree
|
||||
# (fester.py / klanker.py / bridge.js upgraded, Makefile at 0.4.3)
|
||||
# (fester.py / klanker.py / bridge.js upgraded, Makefile at 0.4.4)
|
||||
# - master-build/klanker-gate/ holds the vendored gateway tree + arch/
|
||||
#
|
||||
# Output:
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
# download/ (sandbox mirror)
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="0.4.3"
|
||||
VERSION="0.4.4"
|
||||
NAME="sysdeck-${VERSION}-master"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
STAGE_PARENT="$ROOT/master-build"
|
||||
|
|
@ -33,8 +33,8 @@ echo ">>> Building $NAME"
|
|||
# ── guards: the cockpit tree must be upgraded before packing ──────────
|
||||
grep -q 'start-build' "$STAGE_PARENT/cockpit/bridge/fester.py" || {
|
||||
echo "FAIL: master-build/cockpit/bridge/fester.py is not upgraded (no start-build subcommand)"; exit 1; }
|
||||
grep -q 'VERSION := 0.4.3' "$STAGE_PARENT/cockpit/Makefile" || {
|
||||
echo "FAIL: master-build/cockpit/Makefile is not bumped to 0.4.3"; exit 1; }
|
||||
grep -q 'VERSION := 0.4.4' "$STAGE_PARENT/cockpit/Makefile" || {
|
||||
echo "FAIL: master-build/cockpit/Makefile is not bumped to 0.4.4"; exit 1; }
|
||||
test -f "$STAGE_PARENT/cockpit/bridge/klanker.py" || {
|
||||
echo "FAIL: master-build/cockpit/bridge/klanker.py missing"; exit 1; }
|
||||
grep -q '"journal"' "$STAGE_PARENT/cockpit/bridge/klanker.py" || {
|
||||
|
|
@ -277,13 +277,13 @@ right under Overview) with copy buttons on every command.
|
|||
From the extracted master tarball root, one command does everything
|
||||
(install + migrate + fester + web):
|
||||
|
||||
tar xjf sysdeck-0.4.3-master.tar.bz2
|
||||
cd sysdeck-0.4.3-master
|
||||
tar xjf sysdeck-0.4.4-master.tar.bz2
|
||||
cd sysdeck-0.4.4-master
|
||||
make web-dev # bun install + db:push + fester + next dev :3000
|
||||
|
||||
Granular equivalent (what `make web-dev` does):
|
||||
|
||||
cd sysdeck-0.4.3-master/web
|
||||
cd sysdeck-0.4.4-master/web
|
||||
bun install # dependencies
|
||||
bun run db:push # create + migrate db/custom.db (SQLite)
|
||||
bun run dev # Next.js on :3000
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Master tarball release metadata — reports the sha256/size of the
|
||||
// sysdeck-0.4.3-master bundle in public/download (built by
|
||||
// sysdeck-0.4.4-master bundle in public/download (built by
|
||||
// scripts/make-master-tarball.sh). The tarball itself is served
|
||||
// statically at /download/sysdeck-0.4.3-master.tar.bz2.
|
||||
// statically at /download/sysdeck-0.4.4-master.tar.bz2.
|
||||
//
|
||||
// As of 0.3.1 every web surface is gated; 0.4.0 gates it with the unix-account session
|
||||
// — the tarball file itself stays a plain static
|
||||
|
|
@ -22,7 +22,7 @@ import { requireSession } from '@/lib/sysdeck/session'
|
|||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const FILE = 'sysdeck-0.4.3-master.tar.bz2'
|
||||
const FILE = 'sysdeck-0.4.4-master.tar.bz2'
|
||||
const DOWNLOAD_DIR = path.join(process.cwd(), 'public', 'download')
|
||||
|
||||
// hash cache — recompute when the file size OR mtime changes (rebuild)
|
||||
|
|
@ -46,7 +46,7 @@ export async function GET(req: Request) {
|
|||
}
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
version: '0.4.3',
|
||||
version: '0.4.4',
|
||||
edition: 'master',
|
||||
file: FILE,
|
||||
url: `/download/${FILE}`,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,38 @@ import { db } from '@/lib/db'
|
|||
import { ok, fail, run, readText, which, cached } from './shared'
|
||||
import { readdir, stat } from 'fs/promises'
|
||||
|
||||
async function safeListdir(dir: string): Promise<string[]> {
|
||||
try {
|
||||
return await readdir(dir)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse a zypper pipe-table by locating columns from its header row.
|
||||
* zypper prefixes data tables with status/repository columns whose
|
||||
* count varies by subcommand and release; positional parsing breaks
|
||||
* across those. The header row is the source of truth. */
|
||||
function zypperTable(raw: string, wanted: string[]): { header: Record<string, number>; rows: string[][] } {
|
||||
const header: Record<string, number> = {}
|
||||
const rows: string[][] = []
|
||||
for (const line of raw.split('\n')) {
|
||||
if (!line.includes('|')) continue
|
||||
const cols = line.split('|').map((c) => c.trim())
|
||||
if (Object.keys(header).length === 0) {
|
||||
if (wanted.every((w) => cols.includes(w))) {
|
||||
for (const w of wanted) header[w] = cols.indexOf(w)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Separator rows ('-----+-----') and repeated headers.
|
||||
if (cols.length > 0 && cols.every((c) => c.length > 0 && /^[+\-]+$/.test(c))) continue
|
||||
if (wanted.some((w) => cols[header[w]] === w)) continue
|
||||
rows.push(cols)
|
||||
}
|
||||
return { header, rows }
|
||||
}
|
||||
|
||||
function failE(error: string, source: 'live' | 'hybrid' = 'live') {
|
||||
return { ...fail(error), source }
|
||||
}
|
||||
|
|
@ -431,23 +463,27 @@ const zypperBackend: Backend = {
|
|||
async listUpdates() {
|
||||
const r = await run('zypper', ['-q', 'list-updates'], 60_000)
|
||||
if (r.rc !== 0 && !r.stdout) return null
|
||||
const { header, rows } = zypperTable(r.stdout, ['Name', 'Current', 'Available'])
|
||||
if (header.Name === undefined) return []
|
||||
const out: UpdateRow[] = []
|
||||
for (const line of r.stdout.split('\n')) {
|
||||
if (!line.includes('|')) continue
|
||||
const cols = line.split('|').map((c) => c.trim())
|
||||
if (cols.length < 5 || cols[1] === 'Name' || cols[1] === '') continue
|
||||
out.push({ name: cols[1], current: cols[2], candidate: cols[3] })
|
||||
for (const cols of rows) {
|
||||
const name = cols[header.Name]
|
||||
if (!name || header.Current >= cols.length || header.Available >= cols.length) continue
|
||||
out.push({ name, current: cols[header.Current], candidate: cols[header.Available] })
|
||||
}
|
||||
return out
|
||||
},
|
||||
async search(term) {
|
||||
const r = await run('zypper', ['-q', 'se', term], 30_000)
|
||||
const { header, rows } = zypperTable(r.stdout, ['Name', 'Summary'])
|
||||
const iName = header.Name ?? 1
|
||||
const iSum = header.Summary ?? 2
|
||||
const out: SearchRow[] = []
|
||||
for (const line of r.stdout.split('\n')) {
|
||||
if (!line.includes('|')) continue
|
||||
const cols = line.split('|').map((c) => c.trim())
|
||||
if (cols.length < 3 || cols[1] === 'Name' || !cols[1]) continue
|
||||
out.push({ name: cols[1], version: '', description: cols[2] ?? '', installed: cols[0] === 'i' })
|
||||
for (const cols of rows) {
|
||||
if (cols.length <= Math.max(iName, iSum)) continue
|
||||
const name = cols[iName]
|
||||
if (!name) continue
|
||||
out.push({ name, version: '', description: cols[iSum], installed: cols[0] === 'i' })
|
||||
}
|
||||
return out
|
||||
},
|
||||
|
|
@ -571,8 +607,8 @@ const xbpsBackend: Backend = {
|
|||
const r = await run('xbps-query', ['-Rs', term], 30_000)
|
||||
const out: SearchRow[] = []
|
||||
for (const line of r.stdout.split('\n')) {
|
||||
// "[*] repo/name-ver - description"
|
||||
const m = line.match(/^\[\*\]\s+\S+\/(\S+)\s+-\s+(.*)$/)
|
||||
// "[*] [repo/]name-ver - description" — the repo prefix is optional.
|
||||
const m = line.match(/^\[\*\]\s+(?:\S+\/)?(\S+)\s+-\s+(.*)$/)
|
||||
if (!m) continue
|
||||
const { name, version } = splitNameVer(m[1])
|
||||
out.push({ name, version, description: m[2], installed: false })
|
||||
|
|
@ -635,8 +671,11 @@ const emergeBackend: Backend = {
|
|||
if (r.rc !== 0 && !r.stdout) return null
|
||||
const out: UpdateRow[] = []
|
||||
for (const line of r.stdout.split('\n')) {
|
||||
// " [ebuild U ] dev-lang/python-3.12.4 [3.12.3]"
|
||||
const m = line.match(/\[ebuild\s+U~?\]?\s*([^\s\[]+)\s*(?:\[([^\]]+)\])?/)
|
||||
// " [ebuild U ] cat/pkg-1.2.3 [1.2.2]" — the atom sits AFTER
|
||||
// the class bracket; portage pads the class field with spaces, so
|
||||
// starting the capture before the bracket grabs the bracket itself
|
||||
// and drops every row.
|
||||
const m = line.match(/\[ebuild\s+U[^\]]*\]\s*(\S+)(?:\s+\[([^\]]+)\])?/)
|
||||
if (!m) continue
|
||||
const atom = m[1]
|
||||
const slash = atom.lastIndexOf('/')
|
||||
|
|
@ -664,48 +703,27 @@ const emergeBackend: Backend = {
|
|||
return out
|
||||
},
|
||||
async info(name) {
|
||||
// Real metadata from the installed package's /var/db/pkg entry
|
||||
// Real metadata from the installed package's /var/db/pkg entry.
|
||||
// Accepts 'cat/pkg' atoms and bare names — the latter scans every
|
||||
// category for a matching leaf.
|
||||
const slash = name.lastIndexOf('/')
|
||||
const tryPaths = slash > 0 ? [name] : [name]
|
||||
for (const atom of tryPaths) {
|
||||
let cat = atom.slice(0, slash > 0 ? slash : 0)
|
||||
const leaf = slash > 0 ? atom.slice(slash + 1) : atom
|
||||
if (!cat) {
|
||||
// category-less lookup: scan all categories for a matching name
|
||||
try {
|
||||
const cats = await readdir(EMERGE_PKG_DB)
|
||||
const matches: string[] = []
|
||||
for (const c of cats) {
|
||||
try {
|
||||
const entries = await readdir(`${EMERGE_PKG_DB}/${c}`)
|
||||
matches.push(...entries.filter((pf) => splitNameVer(pf).name === leaf).map((pf) => `${c}/${pf}`))
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (!matches.length) continue
|
||||
cat = matches[0].split('/')[0]
|
||||
const desc = await readText(`${EMERGE_PKG_DB}/${matches[0]}/DESCRIPTION`)
|
||||
const homepage = await readText(`${EMERGE_PKG_DB}/${matches[0]}/HOMEPAGE`)
|
||||
const license = await readText(`${EMERGE_PKG_DB}/${matches[0]}/LICENSE`)
|
||||
const slot = await readText(`${EMERGE_PKG_DB}/${matches[0]}/SLOT`)
|
||||
const use = await readText(`${EMERGE_PKG_DB}/${matches[0]}/USE`)
|
||||
const { version } = splitNameVer(matches[0].split('/')[1])
|
||||
if (desc || version) {
|
||||
return {
|
||||
name: `${cat}/${leaf}`,
|
||||
version,
|
||||
status: 'installed (from /var/db/pkg)',
|
||||
depends: (await readText(`${EMERGE_PKG_DB}/${matches[0]}/RDEPEND`)) || (await readText(`${EMERGE_PKG_DB}/${matches[0]}/PDEPEND`)),
|
||||
description: desc.trim(),
|
||||
maintainer: homepage.trim(),
|
||||
}
|
||||
}
|
||||
void license
|
||||
void slot
|
||||
void use
|
||||
} catch {
|
||||
continue
|
||||
const leaf = slash > 0 ? name.slice(slash + 1) : name
|
||||
const cats = slash > 0 ? [name.slice(0, slash)] : await safeListdir(EMERGE_PKG_DB)
|
||||
for (const cat of cats) {
|
||||
const entries = await safeListdir(`${EMERGE_PKG_DB}/${cat}`)
|
||||
for (const pf of entries) {
|
||||
if (splitNameVer(pf).name !== leaf) continue
|
||||
const pdir = `${EMERGE_PKG_DB}/${cat}/${pf}`
|
||||
const desc = (await readText(`${pdir}/DESCRIPTION`)).trim()
|
||||
const { version } = splitNameVer(pf)
|
||||
if (!desc && !version) continue
|
||||
return {
|
||||
name: `${cat}/${leaf}`,
|
||||
version,
|
||||
status: 'installed (from /var/db/pkg)',
|
||||
depends: (await readText(`${pdir}/RDEPEND`)) || (await readText(`${pdir}/PDEPEND`)),
|
||||
description: desc,
|
||||
maintainer: (await readText(`${pdir}/HOMEPAGE`)).trim(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -875,12 +893,16 @@ const BACKENDS: Record<string, Backend> = {
|
|||
|
||||
const DETECT_ORDER = ['pacman', 'emerge', 'lunar', 'sorcery', 'xbps', 'apk', 'zypper', 'dnf', 'yum', 'apt'] as const
|
||||
|
||||
// Probe binary per manager where the manager id is not itself a binary:
|
||||
// Void ships no bare `xbps` command — xbps-query is the presence probe.
|
||||
const DETECT_PROBES: Record<string, string> = { xbps: 'xbps-query' }
|
||||
|
||||
let detected: { backend: Backend | null; probeAt: number } | null = null
|
||||
|
||||
async function detectBackend(): Promise<Backend | null> {
|
||||
if (detected && Date.now() - detected.probeAt < 300_000) return detected.backend
|
||||
for (const id of DETECT_ORDER) {
|
||||
if (!(await which(id))) continue
|
||||
if (!(await which(DETECT_PROBES[id] ?? id))) continue
|
||||
if (id === 'emerge') {
|
||||
// corroboration: a Gentoo box always has /var/db/pkg
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -72,4 +72,4 @@ export function modulesByGroup(): { group: string; modules: ModuleMeta[] }[] {
|
|||
.filter((g) => g.modules.length > 0)
|
||||
}
|
||||
|
||||
export const SYSDECK_VERSION = '0.4.3'
|
||||
export const SYSDECK_VERSION = '0.4.4'
|
||||
|
|
|
|||
95
worklog.md
95
worklog.md
|
|
@ -1847,3 +1847,98 @@ Work Log:
|
|||
Stage Summary:
|
||||
- One-command quiet cleanup of every cockpit-installed SysDeck version: sudo ./sysdeck-uninstall.sh (or /usr/share/sysdeck/sysdeck-uninstall.sh on installed boxes).
|
||||
- 48/48 harness checks; make check ALL PASS; master tarball rebuilt and verified.
|
||||
|
||||
---
|
||||
Task: v0.4.2 + v0.4.3 catch-up entries (recorded in QA.md; summarized here)
|
||||
Note: the tree worklog missed the 0.4.2/0.4.3 dev cycles (logging lived in
|
||||
the build-side worklog). Full QA detail for both releases is in QA.md:
|
||||
- v0.4.2 (zero-demo release): every web-console module reads real host
|
||||
state; DataSource union loses the 'demo' tier (compiler-enforced);
|
||||
sensors `sensors -j` → sysfs chain; netsec atomic nftables bans with
|
||||
fail2ban merge + real unbans; firewall live-ruleset tab + seven
|
||||
topologies; LUKS header hashes from real image bytes; honest empty
|
||||
inventories with install hints; ten package-manager backends on the
|
||||
web side.
|
||||
- v0.4.3 (MoE QA pass): privileged writes ride stdin with verification;
|
||||
comment injection guards; mktemp staging; admin-gated mutations
|
||||
(SYSDECK_MUTATIONS); honest dry-runs/unbans; XFF trust gating
|
||||
(SYSDECK_TRUST_PROXY); TTL + single-flight caches; cockpit-side
|
||||
bridge parity (sensors chain, dnf rc-100, spawn timeouts); churn
|
||||
wording purged.
|
||||
|
||||
---
|
||||
Task: v0.4.4 — ten package managers on both editions + the blog essay
|
||||
|
||||
Work Log:
|
||||
- bridge/packages.py: ported the web console's ten-backend step-down
|
||||
(pacman, emerge + /var/db/pkg corroboration, lunar, sorcery, xbps
|
||||
via xbps-query probe, apk, zypper, dnf, yum, apt); detection is a
|
||||
shutil.which sweep, no --version children; per-backend read
|
||||
functions (vdb scan for emerge, lvu/gaze with state-file fallbacks,
|
||||
rpm -qa for zypper, yum mirroring dnf with rc-100-as-data); one
|
||||
MUTATION_CMDS table for install/remove/update/update-all/dry-run;
|
||||
lunar single-module update refuses honestly, summary carries
|
||||
updatesNote.
|
||||
- Parser fixes on BOTH editions (found by fixture tests): emerge
|
||||
update regex anchored after the class bracket (the old capture
|
||||
grabbed the bracket and dropped every row — silent "no updates" on
|
||||
Gentoo); zypper tables parse by header-located columns with
|
||||
separator/repeat-header filtering; xbps -Rs rows parse with or
|
||||
without a repository prefix; web emerge info resolves
|
||||
category-qualified atoms too.
|
||||
- web/src/lib/sysdeck/bridge/packages.ts: DETECT_PROBES map (xbps →
|
||||
xbps-query), zypperTable helper, emerge/xbps regex fixes, emerge
|
||||
info rewrite; tsc --noEmit clean, eslint clean.
|
||||
- plugins/sysdeck-packages/packages.js: ten-manager narration,
|
||||
summary.updatesNote rendered, header comment rewritten decisively;
|
||||
node --check clean.
|
||||
- packaging/polkit/org.sysdeck.policy: packages.modify exec-path
|
||||
annotations extended to the ten managers; builder.modify host-query
|
||||
annotations extended; a literal `--` inside an XML comment fixed
|
||||
(strict parsers rejected the file); XML now validates.
|
||||
- bridge/__init__.py: DistroId/PkgManager extended (gentoo, lunar,
|
||||
sourcemage, void, alpine, opensuse) with matching os-release ids
|
||||
and which-based fallbacks.
|
||||
- tests/test_bridge_parsers.py: TestPackagesBackends (13 tests —
|
||||
detection order/probes, emerge corroboration with mocked
|
||||
shutil.which, parsers with fixtures, mutation table coverage, real
|
||||
argv spot-checks, honest lunar summary, no-sudo guard); version
|
||||
sync bumped to 0.4.4. scripts/test_packages_backends.py: standalone
|
||||
fixture suite (10 checks) for quick iteration.
|
||||
- BLOG.md: rebuilt as a single long-form engineering essay following
|
||||
the shellm blog pattern — title, italic deck, context narrative,
|
||||
roadmap, decision-organized sections (PAM auth, one catalog two
|
||||
frontends, the zero-demo contract, ten-manager step-down, firewall
|
||||
privilege discipline, performance without fabrication), canonical
|
||||
numbered workflow, attribution footer. Every path/flag/count
|
||||
verified against source. Release-notes content retired from BLOG.md
|
||||
(history: QA.md + worklog.md; README pointers updated).
|
||||
- Release surfaces: 0.4.4 across Makefile, bridge/__init__.py,
|
||||
packaging (setup.py, PKGBUILD, spec + changelog, debian/changelog),
|
||||
compat-manifest.json, web (package.json, registry.ts, release
|
||||
route.ts, make-master-tarball.sh), README (version + v0.4.4
|
||||
highlights + catalog row + tree comments + pointers), QUICKSTART
|
||||
§10.8, QA.md v0.4.4 entry.
|
||||
- Incident + fix (same class as the v0.0.44 one): the Edit tool
|
||||
converted Makefile recipe TABs to 8 spaces across 584 lines when the
|
||||
master rule was edited; `make` failed with "missing separator".
|
||||
Repaired by restoring the pristine Makefile from the shipped 0.4.3
|
||||
tarball and re-applying the three intended changes (version bump,
|
||||
master-rule prefix/klanker-gate/tsbuildinfo/worklog.md) through a
|
||||
tab-preserving Python patch; make check ALL PASS after, including
|
||||
the recipe-indentation guard.
|
||||
- Makefile master rule corrected while there: the bundle prefix is
|
||||
`$(PACKAGE)-$(VERSION)-master/` (the rule's old transform dropped
|
||||
the -master suffix), the vendored klanker-gate/ tree and worklog.md
|
||||
are in the file list, and *.tsbuildinfo is excluded — `make master`
|
||||
now reproduces the shipped bundle shape byte-for-byte in content
|
||||
(960 entries, one intentional addition: the packages fixture suite).
|
||||
- Verification: py_compile all bridges; fixture suite 10/10;
|
||||
TestPackagesBackends 13/13; node --check panel JS; polkit XML
|
||||
validated; tsc --noEmit clean; eslint clean; make check full run;
|
||||
master tarball rebuilt via make master.
|
||||
|
||||
Stage Summary:
|
||||
- Package module parity complete: ten managers, identical step-down
|
||||
and parsers, both editions, fixture-locked.
|
||||
- BLOG.md is the engineering essay the project always pointed at.
|
||||
|
|
|
|||
Loading…
Reference in New Issue