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.
|
# Distro support: Arch Linux, Debian/Ubuntu, Fedora/RHEL/CentOS.
|
||||||
|
|
||||||
PACKAGE := sysdeck
|
PACKAGE := sysdeck
|
||||||
VERSION := 0.4.3
|
VERSION := 0.4.4
|
||||||
LIB_DIR := $(DESTDIR)/usr/lib/$(PACKAGE)
|
LIB_DIR := $(DESTDIR)/usr/lib/$(PACKAGE)
|
||||||
PYTHON_DIR := $(LIB_DIR)/bridge
|
PYTHON_DIR := $(LIB_DIR)/bridge
|
||||||
SHARE_DIR := $(DESTDIR)/usr/share/$(PACKAGE)
|
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)"
|
@echo ">>> Starting SysDeck Web Edition on :3000 (Ctrl+C stops next; fester keeps running)"
|
||||||
cd web && bun run dev
|
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:
|
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 \
|
tar cjf $(PACKAGE)-$(VERSION)-master.tar.bz2 \
|
||||||
--exclude='__pycache__' --exclude='*.pyc' --exclude='*.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-*' \
|
--exclude='*public/download*' --exclude='*.db' --exclude='*.db-*' \
|
||||||
--transform 's,^,$(PACKAGE)-$(VERSION)-,' \
|
--transform 's,^,$(PACKAGE)-$(VERSION)-master/,' \
|
||||||
bridge plugins shared tests packaging compat standalone-plugins \
|
bridge plugins shared tests packaging compat standalone-plugins \
|
||||||
prometheus scripts firewall docs web \
|
prometheus scripts firewall docs web klanker-gate \
|
||||||
Makefile README.md QUICKSTART.md BLOG.md LICENSE QA.md THIRD_PARTY.md \
|
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
|
sysdeck-diagnose.sh cockpit-smoke-test.sh sysdeck-uninstall.sh
|
||||||
@echo ">>> $(PACKAGE)-$(VERSION)-master.tar.bz2 ready"
|
@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)
|
# MoE Quality Assurance Pass — v0.4.3 (the hardened release)
|
||||||
|
|
||||||
## v0.4.3 QA — multi-expert audit, findings landed
|
## 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)
|
## 11. Run without Cockpit (the complete standalone runbook, v0.3.0)
|
||||||
|
|
||||||
The web edition needs **nothing from sections 1–8** — no cockpit, no Python
|
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.**
|
**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>
|
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.
|
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)
|
### 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
|
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 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.
|
- **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`.
|
- **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`.
|
- **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 |
|
| 15 | System Monitor (Glances web UI) | `cockpit-glances` | P1 | `glances -w` (iframe) + snapshot cards |
|
||||||
| 16 | Hardware Sensors | `cockpit-sensors` | P1 | `sensors` (lm_sensors) |
|
| 16 | Hardware Sensors | `cockpit-sensors` | P1 | `sensors` (lm_sensors) |
|
||||||
| 17 | System Benchmark | `cockpit-benchmark` | P2 | `sysbench` |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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-mesh/
|
||||||
│ ├── sysdeck-vault/
|
│ ├── sysdeck-vault/
|
||||||
│ ├── sysdeck-fleet/
|
│ ├── 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-fester/
|
||||||
│ ├── sysdeck-firmware/
|
│ ├── sysdeck-firmware/
|
||||||
│ ├── sysdeck-builder/
|
│ ├── sysdeck-builder/
|
||||||
|
|
@ -519,7 +557,7 @@ sysdeck-0.0.35/
|
||||||
│ ├── glances.py # v0.0.34: snapshot + start-web/stop-web
|
│ ├── glances.py # v0.0.34: snapshot + start-web/stop-web
|
||||||
│ ├── sensors.py # lm_sensors normalization + alert thresholds
|
│ ├── sensors.py # lm_sensors normalization + alert thresholds
|
||||||
│ ├── benchmark.py # sysbench result parsing + baselines
|
│ ├── 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
|
│ ├── mining.py # v0.0.34: XMRig REST API power tool
|
||||||
│ ├── themes.py # v0.0.34: cockpit.conf + CSS variable surface
|
│ ├── themes.py # v0.0.34: cockpit.conf + CSS variable surface
|
||||||
│ ├── policy.py # Policy & Permissions module (LSM stack)
|
│ ├── policy.py # Policy & Permissions module (LSM stack)
|
||||||
|
|
@ -543,7 +581,7 @@ sysdeck-0.0.35/
|
||||||
├── docs/ # INSTALL.md
|
├── docs/ # INSTALL.md
|
||||||
├── README.md
|
├── README.md
|
||||||
├── QUICKSTART.md
|
├── QUICKSTART.md
|
||||||
├── BLOG.md # release narrative
|
├── BLOG.md # engineering essay (long-form)
|
||||||
├── QA.md # QA notes per release
|
├── QA.md # QA notes per release
|
||||||
├── THIRD_PARTY.md # third-party attributions
|
├── THIRD_PARTY.md # third-party attributions
|
||||||
├── LICENSE # MIT
|
├── LICENSE # MIT
|
||||||
|
|
@ -616,7 +654,7 @@ MIT — see [LICENSE](./LICENSE). Third-party attributions: see [THIRD_PARTY.md]
|
||||||
|
|
||||||
## Release notes
|
## 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
|
## Project history
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
__version__ = "0.4.3"
|
__version__ = "0.4.4"
|
||||||
__author__ = "Jeremy Anderson"
|
__author__ = "Jeremy Anderson"
|
||||||
__url__ = "https://dcos.net"
|
__url__ = "https://dcos.net"
|
||||||
|
|
||||||
|
|
@ -33,7 +33,9 @@ __url__ = "https://dcos.net"
|
||||||
# then fall back to checking which package manager is available.
|
# then fall back to checking which package manager is available.
|
||||||
# Returns a normalized distro identifier for use in dispatch tables.
|
# 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:
|
def detect_distro() -> DistroId:
|
||||||
|
|
@ -42,9 +44,12 @@ def detect_distro() -> DistroId:
|
||||||
Priority order:
|
Priority order:
|
||||||
1. Parse /etc/os-release ID/ID_LIKE fields.
|
1. Parse /etc/os-release ID/ID_LIKE fields.
|
||||||
2. Fall back to package-manager presence (pacman → arch,
|
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).
|
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 /etc/os-release first (present on all modern distros).
|
||||||
try:
|
try:
|
||||||
|
|
@ -59,6 +64,12 @@ def detect_distro() -> DistroId:
|
||||||
|
|
||||||
# Direct match on ID.
|
# Direct match on ID.
|
||||||
id_map = {"arch": "arch", "archlinux": "arch",
|
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",
|
"debian": "debian", "ubuntu": "debian", "linuxmint": "debian", "pop": "debian",
|
||||||
"fedora": "fedora", "rhel": "rhel", "centos": "rhel", "rocky": "rhel", "alma": "rhel"}
|
"fedora": "fedora", "rhel": "rhel", "centos": "rhel", "rocky": "rhel", "alma": "rhel"}
|
||||||
if dist_id in id_map:
|
if dist_id in id_map:
|
||||||
|
|
@ -72,7 +83,11 @@ def detect_distro() -> DistroId:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fall back to package manager presence.
|
# 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:
|
try:
|
||||||
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
||||||
return distro
|
return distro
|
||||||
|
|
@ -88,16 +103,25 @@ DISTRO: DistroId = detect_distro()
|
||||||
|
|
||||||
# ── Package manager detection ───────────────────────────────────────
|
# ── Package manager detection ───────────────────────────────────────
|
||||||
#
|
#
|
||||||
# Returns the command name for the system's package manager.
|
# Returns the manager id for the system's distro, matching the ten
|
||||||
# Arch → pacman, Debian → apt, Fedora/RHEL → dnf.
|
# 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:
|
def detect_pkg_manager() -> PkgManager:
|
||||||
"""Detect the system package manager based on distro."""
|
"""Detect the system package manager based on distro."""
|
||||||
pkg_map: dict[DistroId, PkgManager] = {
|
pkg_map: dict[DistroId, PkgManager] = {
|
||||||
"arch": "pacman",
|
"arch": "pacman",
|
||||||
|
"gentoo": "emerge",
|
||||||
|
"lunar": "lunar",
|
||||||
|
"sourcemage": "sorcery",
|
||||||
|
"void": "xbps",
|
||||||
|
"alpine": "apk",
|
||||||
|
"opensuse": "zypper",
|
||||||
"debian": "apt",
|
"debian": "apt",
|
||||||
"fedora": "dnf",
|
"fedora": "dnf",
|
||||||
"rhel": "dnf",
|
"rhel": "dnf",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"_comment": "Compatibility Manifest — sysdeck v0.1.3",
|
"_comment": "Compatibility Manifest — sysdeck v0.1.3",
|
||||||
"version": "0.4.3",
|
"version": "0.4.4",
|
||||||
"suite_requires": { "cockpit": ">=239", "python": ">=3.9" },
|
"suite_requires": { "cockpit": ">=239", "python": ">=3.9" },
|
||||||
"modules": {
|
"modules": {
|
||||||
"containers": {
|
"containers": {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
# Upstream: https://dcos.net
|
# Upstream: https://dcos.net
|
||||||
|
|
||||||
pkgname=sysdeck
|
pkgname=sysdeck
|
||||||
pkgver=0.4.3
|
pkgver=0.4.4
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Unified operations surface for Linux infrastructure — twenty-six domain modules behind one Cockpit dashboard"
|
pkgdesc="Unified operations surface for Linux infrastructure — twenty-six domain modules behind one Cockpit dashboard"
|
||||||
arch=('any')
|
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
|
sysdeck (0.4.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
* v0.4.3: the MoE QA pass — production hardening across every axis.
|
* v0.4.3: the MoE QA pass — production hardening across every axis.
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,8 @@
|
||||||
</action>
|
</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. -->
|
<!-- Read-only operations (list, search, info) do not need this. -->
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
<action id="org.sysdeck.packages.modify">
|
<action id="org.sysdeck.packages.modify">
|
||||||
|
|
@ -105,6 +106,12 @@
|
||||||
<allow_active>auth_admin_keep</allow_active>
|
<allow_active>auth_admin_keep</allow_active>
|
||||||
</defaults>
|
</defaults>
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/pacman</annotate>
|
<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/apt</annotate>
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/dnf</annotate>
|
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/dnf</annotate>
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/yum</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 -->
|
<!-- /var/lib/sysdeck/builder/. The bridge runs the backend via -->
|
||||||
<!-- subprocess; the cockpit superuser channel handles root priv. -->
|
<!-- subprocess; the cockpit superuser channel handles root priv. -->
|
||||||
<!-- v0.1.0: profile-import-packages also queries the host's -->
|
<!-- v0.1.0: profile-import-packages also queries the host's -->
|
||||||
<!-- package manager (pacman -Qqe / apt-mark showmanual / -->
|
<!-- package manager (pacman -Qqe / apt-mark showmanual / the dnf -->
|
||||||
<!-- dnf repoquery --userinstalled) to capture the operator's -->
|
<!-- repoquery userinstalled filter) to capture the operator's -->
|
||||||
<!-- explicitly-installed package set. -->
|
<!-- explicitly-installed package set. -->
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
<action id="org.sysdeck.builder.modify">
|
<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/pacman</annotate>
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/apt-mark</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/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>
|
</action>
|
||||||
|
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
VERSION = "0.4.3"
|
VERSION = "0.4.4"
|
||||||
PACKAGE = "sysdeck"
|
PACKAGE = "sysdeck"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
# Debian/Ubuntu users: see packaging/debian/
|
# Debian/Ubuntu users: see packaging/debian/
|
||||||
|
|
||||||
Name: sysdeck
|
Name: sysdeck
|
||||||
Version: 0.4.3
|
Version: 0.4.4
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Unified operations surface for Linux infrastructure
|
Summary: Unified operations surface for Linux infrastructure
|
||||||
|
|
||||||
|
|
@ -84,6 +84,16 @@ if [ $1 -eq 0 ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
%changelog
|
%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
|
* 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.
|
- v0.4.3: the MoE QA pass — production hardening across every axis.
|
||||||
Privileged writes ride stdin (polkit rules verified post-write,
|
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)
|
* Author: Jeremy Anderson (https://dcos.net)
|
||||||
*
|
*
|
||||||
* Package management panel — list, search, install, update, and remove
|
* 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 —
|
* The package manager is invoked as a separate process via cockpit.spawn —
|
||||||
* no package-manager code is bundled.
|
* no package-manager code is bundled.
|
||||||
*
|
*
|
||||||
* v0.0.31 REWRITE — UPDATE NEEDS SUDO, FIXED THE COCKPIT WAY.
|
* Mutations (install/remove/update/update-all) execute via the cockpit
|
||||||
* v0.0.30 packages.js Update All button called bridge.packages.updateAll()
|
* superuser channel (polkit): the bridge helper runs the detected
|
||||||
* which returned only the command string that *would* be run. The panel
|
* package manager via subprocess, and this panel subscribes to the
|
||||||
* showed `alert("Run this command with superuser privileges.")` and the
|
* cockpit spawn stream so the operator sees live stdout/stderr in a
|
||||||
* operator had to copy the command, open a terminal, sudo, paste, run.
|
* <pre> log panel — exactly like cockpit's own Packages and Software
|
||||||
* That defeated the purpose of having a panel.
|
* Updates panels. No `sudo` shell-out from JS.
|
||||||
*
|
*
|
||||||
* v0.0.31 makes install/remove/update/update-all actually execute via
|
* SECURITY: every dynamic string interpolated into innerHTML
|
||||||
* 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
|
|
||||||
* (package names, versions, descriptions, search terms echoed back,
|
* (package names, versions, descriptions, search terms echoed back,
|
||||||
* error messages) now goes through escapeHtml(). Package metadata is
|
* error messages) goes through escapeHtml(). Package metadata is
|
||||||
* live data from pacman/dnf/apt output — a typo-squat repo or a
|
* live data from the package manager's output — a typo-squat repo or
|
||||||
* locally-installed package whose name/description contains markup
|
* a locally-installed package whose name/description contains markup
|
||||||
* used to execute in the cockpit admin session (0.3.0 audit).
|
* must not execute in the cockpit admin session (0.3.0 audit).
|
||||||
* Raw tool output already flows through textContent (showOutput),
|
* Raw tool output already flows through textContent (showOutput),
|
||||||
* which is safe.
|
* which is safe.
|
||||||
*/
|
*/
|
||||||
|
|
@ -59,11 +55,16 @@ export async function mount(panel, { bridge, EventBus }) {
|
||||||
const instCount = summary.installedCount || installed.length;
|
const instCount = summary.installedCount || installed.length;
|
||||||
const updCount = summary.updateCount || 0;
|
const updCount = summary.updateCount || 0;
|
||||||
const updates = summary.updates || [];
|
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 = `
|
panel.innerHTML = `
|
||||||
<header>
|
<header>
|
||||||
<h2 class="suite-panel-title">Package Manager</h2>
|
<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>
|
</header>
|
||||||
<div class="suite-row">
|
<div class="suite-row">
|
||||||
<div class="suite-card suite-col-2">
|
<div class="suite-card suite-col-2">
|
||||||
|
|
@ -220,6 +221,6 @@ function renderSkeleton() {
|
||||||
function renderError(err) {
|
function renderError(err) {
|
||||||
return `<div class="suite-card">
|
return `<div class="suite-card">
|
||||||
<h3 class="suite-card-title">Package manager unavailable</h3>
|
<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>`;
|
</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):
|
def test_version_sync_all_surfaces_report_020(self):
|
||||||
# Every release surface must report v0.4.3 (MoE QA production pass).
|
# Every release surface must report v0.4.4 (package parity + blog essay).
|
||||||
v = "0.4.3"
|
v = "0.4.4"
|
||||||
files_to_check = [
|
files_to_check = [
|
||||||
"Makefile",
|
"Makefile",
|
||||||
"bridge/__init__.py",
|
"bridge/__init__.py",
|
||||||
|
|
@ -1934,6 +1934,177 @@ class TestFirewallV047ManifestsAndMetainfo(unittest.TestCase):
|
||||||
f"{rel} does not reference version {v}")
|
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):
|
class TestBuilderProfileCopy(unittest.TestCase):
|
||||||
"""v0.0.48: unit tests for bridge.builder.profile_copy().
|
"""v0.0.48: unit tests for bridge.builder.profile_copy().
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "nextjs_tailwind_shadcn_ts",
|
"name": "nextjs_tailwind_shadcn_ts",
|
||||||
"version": "0.4.3",
|
"version": "0.4.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -H 127.0.0.1 -p 3000 2>&1 | tee dev.log",
|
"dev": "next dev -H 127.0.0.1 -p 3000 2>&1 | tee dev.log",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
#
|
#
|
||||||
# Preconditions:
|
# Preconditions:
|
||||||
# - master-build/cockpit/ holds the staged + upgraded cockpit tree
|
# - 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/
|
# - master-build/klanker-gate/ holds the vendored gateway tree + arch/
|
||||||
#
|
#
|
||||||
# Output:
|
# Output:
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
# download/ (sandbox mirror)
|
# download/ (sandbox mirror)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="0.4.3"
|
VERSION="0.4.4"
|
||||||
NAME="sysdeck-${VERSION}-master"
|
NAME="sysdeck-${VERSION}-master"
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
STAGE_PARENT="$ROOT/master-build"
|
STAGE_PARENT="$ROOT/master-build"
|
||||||
|
|
@ -33,8 +33,8 @@ echo ">>> Building $NAME"
|
||||||
# ── guards: the cockpit tree must be upgraded before packing ──────────
|
# ── guards: the cockpit tree must be upgraded before packing ──────────
|
||||||
grep -q 'start-build' "$STAGE_PARENT/cockpit/bridge/fester.py" || {
|
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; }
|
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" || {
|
grep -q 'VERSION := 0.4.4' "$STAGE_PARENT/cockpit/Makefile" || {
|
||||||
echo "FAIL: master-build/cockpit/Makefile is not bumped to 0.4.3"; exit 1; }
|
echo "FAIL: master-build/cockpit/Makefile is not bumped to 0.4.4"; exit 1; }
|
||||||
test -f "$STAGE_PARENT/cockpit/bridge/klanker.py" || {
|
test -f "$STAGE_PARENT/cockpit/bridge/klanker.py" || {
|
||||||
echo "FAIL: master-build/cockpit/bridge/klanker.py missing"; exit 1; }
|
echo "FAIL: master-build/cockpit/bridge/klanker.py missing"; exit 1; }
|
||||||
grep -q '"journal"' "$STAGE_PARENT/cockpit/bridge/klanker.py" || {
|
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
|
From the extracted master tarball root, one command does everything
|
||||||
(install + migrate + fester + web):
|
(install + migrate + fester + web):
|
||||||
|
|
||||||
tar xjf sysdeck-0.4.3-master.tar.bz2
|
tar xjf sysdeck-0.4.4-master.tar.bz2
|
||||||
cd sysdeck-0.4.3-master
|
cd sysdeck-0.4.4-master
|
||||||
make web-dev # bun install + db:push + fester + next dev :3000
|
make web-dev # bun install + db:push + fester + next dev :3000
|
||||||
|
|
||||||
Granular equivalent (what `make web-dev` does):
|
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 install # dependencies
|
||||||
bun run db:push # create + migrate db/custom.db (SQLite)
|
bun run db:push # create + migrate db/custom.db (SQLite)
|
||||||
bun run dev # Next.js on :3000
|
bun run dev # Next.js on :3000
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Master tarball release metadata — reports the sha256/size of the
|
// 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
|
// 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
|
// 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
|
// — the tarball file itself stays a plain static
|
||||||
|
|
@ -22,7 +22,7 @@ import { requireSession } from '@/lib/sysdeck/session'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
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')
|
const DOWNLOAD_DIR = path.join(process.cwd(), 'public', 'download')
|
||||||
|
|
||||||
// hash cache — recompute when the file size OR mtime changes (rebuild)
|
// hash cache — recompute when the file size OR mtime changes (rebuild)
|
||||||
|
|
@ -46,7 +46,7 @@ export async function GET(req: Request) {
|
||||||
}
|
}
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
version: '0.4.3',
|
version: '0.4.4',
|
||||||
edition: 'master',
|
edition: 'master',
|
||||||
file: FILE,
|
file: FILE,
|
||||||
url: `/download/${FILE}`,
|
url: `/download/${FILE}`,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,38 @@ import { db } from '@/lib/db'
|
||||||
import { ok, fail, run, readText, which, cached } from './shared'
|
import { ok, fail, run, readText, which, cached } from './shared'
|
||||||
import { readdir, stat } from 'fs/promises'
|
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') {
|
function failE(error: string, source: 'live' | 'hybrid' = 'live') {
|
||||||
return { ...fail(error), source }
|
return { ...fail(error), source }
|
||||||
}
|
}
|
||||||
|
|
@ -431,23 +463,27 @@ const zypperBackend: Backend = {
|
||||||
async listUpdates() {
|
async listUpdates() {
|
||||||
const r = await run('zypper', ['-q', 'list-updates'], 60_000)
|
const r = await run('zypper', ['-q', 'list-updates'], 60_000)
|
||||||
if (r.rc !== 0 && !r.stdout) return null
|
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[] = []
|
const out: UpdateRow[] = []
|
||||||
for (const line of r.stdout.split('\n')) {
|
for (const cols of rows) {
|
||||||
if (!line.includes('|')) continue
|
const name = cols[header.Name]
|
||||||
const cols = line.split('|').map((c) => c.trim())
|
if (!name || header.Current >= cols.length || header.Available >= cols.length) continue
|
||||||
if (cols.length < 5 || cols[1] === 'Name' || cols[1] === '') continue
|
out.push({ name, current: cols[header.Current], candidate: cols[header.Available] })
|
||||||
out.push({ name: cols[1], current: cols[2], candidate: cols[3] })
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
},
|
},
|
||||||
async search(term) {
|
async search(term) {
|
||||||
const r = await run('zypper', ['-q', 'se', term], 30_000)
|
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[] = []
|
const out: SearchRow[] = []
|
||||||
for (const line of r.stdout.split('\n')) {
|
for (const cols of rows) {
|
||||||
if (!line.includes('|')) continue
|
if (cols.length <= Math.max(iName, iSum)) continue
|
||||||
const cols = line.split('|').map((c) => c.trim())
|
const name = cols[iName]
|
||||||
if (cols.length < 3 || cols[1] === 'Name' || !cols[1]) continue
|
if (!name) continue
|
||||||
out.push({ name: cols[1], version: '', description: cols[2] ?? '', installed: cols[0] === 'i' })
|
out.push({ name, version: '', description: cols[iSum], installed: cols[0] === 'i' })
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
},
|
},
|
||||||
|
|
@ -571,8 +607,8 @@ const xbpsBackend: Backend = {
|
||||||
const r = await run('xbps-query', ['-Rs', term], 30_000)
|
const r = await run('xbps-query', ['-Rs', term], 30_000)
|
||||||
const out: SearchRow[] = []
|
const out: SearchRow[] = []
|
||||||
for (const line of r.stdout.split('\n')) {
|
for (const line of r.stdout.split('\n')) {
|
||||||
// "[*] repo/name-ver - description"
|
// "[*] [repo/]name-ver - description" — the repo prefix is optional.
|
||||||
const m = line.match(/^\[\*\]\s+\S+\/(\S+)\s+-\s+(.*)$/)
|
const m = line.match(/^\[\*\]\s+(?:\S+\/)?(\S+)\s+-\s+(.*)$/)
|
||||||
if (!m) continue
|
if (!m) continue
|
||||||
const { name, version } = splitNameVer(m[1])
|
const { name, version } = splitNameVer(m[1])
|
||||||
out.push({ name, version, description: m[2], installed: false })
|
out.push({ name, version, description: m[2], installed: false })
|
||||||
|
|
@ -635,8 +671,11 @@ const emergeBackend: Backend = {
|
||||||
if (r.rc !== 0 && !r.stdout) return null
|
if (r.rc !== 0 && !r.stdout) return null
|
||||||
const out: UpdateRow[] = []
|
const out: UpdateRow[] = []
|
||||||
for (const line of r.stdout.split('\n')) {
|
for (const line of r.stdout.split('\n')) {
|
||||||
// " [ebuild U ] dev-lang/python-3.12.4 [3.12.3]"
|
// " [ebuild U ] cat/pkg-1.2.3 [1.2.2]" — the atom sits AFTER
|
||||||
const m = line.match(/\[ebuild\s+U~?\]?\s*([^\s\[]+)\s*(?:\[([^\]]+)\])?/)
|
// 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
|
if (!m) continue
|
||||||
const atom = m[1]
|
const atom = m[1]
|
||||||
const slash = atom.lastIndexOf('/')
|
const slash = atom.lastIndexOf('/')
|
||||||
|
|
@ -664,48 +703,27 @@ const emergeBackend: Backend = {
|
||||||
return out
|
return out
|
||||||
},
|
},
|
||||||
async info(name) {
|
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 slash = name.lastIndexOf('/')
|
||||||
const tryPaths = slash > 0 ? [name] : [name]
|
const leaf = slash > 0 ? name.slice(slash + 1) : name
|
||||||
for (const atom of tryPaths) {
|
const cats = slash > 0 ? [name.slice(0, slash)] : await safeListdir(EMERGE_PKG_DB)
|
||||||
let cat = atom.slice(0, slash > 0 ? slash : 0)
|
for (const cat of cats) {
|
||||||
const leaf = slash > 0 ? atom.slice(slash + 1) : atom
|
const entries = await safeListdir(`${EMERGE_PKG_DB}/${cat}`)
|
||||||
if (!cat) {
|
for (const pf of entries) {
|
||||||
// category-less lookup: scan all categories for a matching name
|
if (splitNameVer(pf).name !== leaf) continue
|
||||||
try {
|
const pdir = `${EMERGE_PKG_DB}/${cat}/${pf}`
|
||||||
const cats = await readdir(EMERGE_PKG_DB)
|
const desc = (await readText(`${pdir}/DESCRIPTION`)).trim()
|
||||||
const matches: string[] = []
|
const { version } = splitNameVer(pf)
|
||||||
for (const c of cats) {
|
if (!desc && !version) continue
|
||||||
try {
|
return {
|
||||||
const entries = await readdir(`${EMERGE_PKG_DB}/${c}`)
|
name: `${cat}/${leaf}`,
|
||||||
matches.push(...entries.filter((pf) => splitNameVer(pf).name === leaf).map((pf) => `${c}/${pf}`))
|
version,
|
||||||
} catch {
|
status: 'installed (from /var/db/pkg)',
|
||||||
continue
|
depends: (await readText(`${pdir}/RDEPEND`)) || (await readText(`${pdir}/PDEPEND`)),
|
||||||
}
|
description: desc,
|
||||||
}
|
maintainer: (await readText(`${pdir}/HOMEPAGE`)).trim(),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -875,12 +893,16 @@ const BACKENDS: Record<string, Backend> = {
|
||||||
|
|
||||||
const DETECT_ORDER = ['pacman', 'emerge', 'lunar', 'sorcery', 'xbps', 'apk', 'zypper', 'dnf', 'yum', 'apt'] as const
|
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
|
let detected: { backend: Backend | null; probeAt: number } | null = null
|
||||||
|
|
||||||
async function detectBackend(): Promise<Backend | null> {
|
async function detectBackend(): Promise<Backend | null> {
|
||||||
if (detected && Date.now() - detected.probeAt < 300_000) return detected.backend
|
if (detected && Date.now() - detected.probeAt < 300_000) return detected.backend
|
||||||
for (const id of DETECT_ORDER) {
|
for (const id of DETECT_ORDER) {
|
||||||
if (!(await which(id))) continue
|
if (!(await which(DETECT_PROBES[id] ?? id))) continue
|
||||||
if (id === 'emerge') {
|
if (id === 'emerge') {
|
||||||
// corroboration: a Gentoo box always has /var/db/pkg
|
// corroboration: a Gentoo box always has /var/db/pkg
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -72,4 +72,4 @@ export function modulesByGroup(): { group: string; modules: ModuleMeta[] }[] {
|
||||||
.filter((g) => g.modules.length > 0)
|
.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:
|
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).
|
- 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.
|
- 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