{
  "schema": 1,
  "generated": "2026-09-04T19:42:12.741Z",
  "family": {
    "status": "beta",
    "stable_from": null,
    "notice": "Beta: the family is days old and still changing. Package names, flags and keys may move without notice until 1.0. Pin versions, and report what breaks."
  },
  "packages": {
    "repo": "https://pkgs.tui.tools",
    "install_script": "https://pkgs.tui.tools/install.sh",
    "live": true,
    "deb": "deb [signed-by=/etc/apt/keyrings/tui-tools.gpg] https://pkgs.tui.tools/deb stable main",
    "rpm": "https://pkgs.tui.tools/rpm/tui-tools.repo",
    "arch": "Server = https://pkgs.tui.tools/arch/$arch"
  },
  "signing": {
    "pubkey": "https://pkgs.tui.tools/pubkey.asc"
  },
  "tools": [
    {
      "name": "tui-cert",
      "package": "tui-cert",
      "binary": "tui-cert",
      "tagline": "Every certificate on the machine, and what will stop working when",
      "description": "A terminal UI for the TLS certificates on this machine. It finds them — the Let's Encrypt tree, /etc/ssl, /etc/pki, the paths an nginx, Apache or Caddy configuration names — and puts them on one screen, worst first: expired, expiring, the ones whose private key is not their key, the ones whose key every account can read.\n\nReading is done in Go. crypto/x509 parses the chain, verifies it against the system trust store and compares the key, so the whole inventory works on a machine with nothing else installed. openssl, certbot and acme.sh are optional, and only used to act.\n\nTwo more screens complete it: what certbot or acme.sh manages and whether its timer is really running, and a live TLS handshake on demand — how a certificate renewed on disk and never reloaded shows itself.\n\nEvery change is previewed as the exact command line. `I` obtains a certificate this machine does not have yet, by webroot or standalone, and requests nothing until the agreement says yes. `i` installs a pair to the paths a server's configuration names — chosen from that list, never typed — at 644 and 600, then reloads it.\n\nRun `tui-cert --demo` for a sample machine with seven certificates in seven states.",
      "category": "certificates",
      "icon": "https://tui.tools/tools/tui-cert/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-cert/tui-cert-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:54:28Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "certbot",
        "acme-sh",
        "openssl"
      ],
      "compat": {
        "certbot": {
          "binary": "certbot",
          "minimum": "1.0",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": ">=1.0",
              "impact": "certbot is optional: without it the inventory, the chain validation, the key match and the live check all still work, and only the renewal screen's two keys say there is nothing here to renew with"
            },
            {
              "range": "<2.0",
              "impact": "a certificate renewed by this certbot keeps the RSA key type it was issued with; from 2.0 the default became ECDSA, so a renewal there can change the key type of a certificate a client was pinned to"
            }
          ]
        },
        "acme-sh": {
          "binary": "acme.sh",
          "minimum": "3.0.0",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": ">=3.0.0",
              "impact": "acme.sh renews from a crontab entry rather than a systemd timer, and tui-cert reads nobody's crontab: the renewal screen says so instead of claiming nothing is scheduled"
            }
          ]
        },
        "openssl": {
          "binary": "openssl",
          "minimum": "1.1.1",
          "tested": [
            "3.0.13",
            "3.5.5",
            "3.6.4"
          ],
          "features": [
            {
              "name": "addext",
              "since": "1.1.1"
            }
          ],
          "notes": [
            {
              "range": "<1.1.1",
              "impact": "`openssl req` has no `-addext`, so a subject alternative name cannot be set on the command line; tui-cert will not write an openssl configuration file, so the generate keys say why they are unavailable rather than producing a certificate no client accepts"
            },
            {
              "range": ">=1.1.1",
              "impact": "openssl is only ever used to generate a key pair; nothing is read with it, so a machine without it loses two keys and keeps every screen"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-cert",
      "page": "https://tui.tools/tools/tui-cert/",
      "changelog": "https://github.com/tui-tools/tui-cert/releases/tag/v0.2.1"
    },
    {
      "name": "tui-containers",
      "package": "tui-containers",
      "binary": "tui-containers",
      "tagline": "Every container on one screen, docker and podman together",
      "description": "A terminal UI for the containers on a machine. Docker and Podman are two engines with two stores, two sets of commands and — for Podman — two scopes that do not see each other; tui-containers puts them on one list, worst first: the ones exited non-zero, the ones stuck restarting, the ones their own health check disagrees with.\n\nA container is more than its row. Open one and it says what it is using, what it is mounted on, what its health check has been printing, and its environment — with any value whose name says it is a secret replaced, and the name still on screen. Logs are a pane, re-read on a timer rather than followed.\n\nChanges are previewed as the exact command line: the lifecycle verbs, remove, `update --restart`, a pull, `compose up -d`/`down`/`pull` from the labels Compose wrote, and every prune as a sentence, not a flag.\n\n`N` runs a new container from an image — name, ports, mounts, an `--env-file` path, a restart policy — showing the whole `run -d` line first. `n` creates a volume or a network. There is no inline environment value: a secret typed into a form would sit in the preview and in `inspect`.\n\nRun `tui-containers --demo` for a sample machine with both engines.",
      "category": "containers",
      "icon": "https://tui.tools/tools/tui-containers/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-containers/tui-containers-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:53:39Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "docker",
        "podman"
      ],
      "compat": {
        "docker": {
          "binary": "docker",
          "minimum": "20.10",
          "tested": [],
          "features": [
            {
              "name": "format-json",
              "since": "23.0"
            }
          ],
          "notes": [
            {
              "range": "<23.0",
              "impact": "`--format json` as a shorthand does not exist, so every list is read with `--format '{{json .}}'` instead. Nothing is lost: it is the same output, and it is what the shorthand expands to"
            },
            {
              "range": ">=20.10",
              "impact": "`docker --version` answers without a daemon, which is why it is the version probe: a machine with the CLI installed and dockerd stopped reports its version and then reports the daemon as not answering, with the reason"
            },
            {
              "range": ">=20.10",
              "impact": "the label list `docker ps` prints is every label joined with commas and nothing is escaped, so a label value containing a comma can be mis-split. The Compose labels this tool reads never contain one"
            },
            {
              "range": ">=20.10",
              "impact": "reading needs whatever the docker socket needs: an account in the `docker` group or a rootless daemon reaches it directly, and otherwise the tool retries the probe once through `sudo -n` and says in the header that it did"
            }
          ]
        },
        "podman": {
          "binary": "podman",
          "minimum": "4.0",
          "tested": [
            "5.8.1"
          ],
          "features": [
            {
              "name": "quadlet",
              "since": "4.4"
            },
            {
              "name": "compose",
              "since": "4.7"
            },
            {
              "name": "update-restart",
              "since": "5.0"
            }
          ],
          "notes": [
            {
              "range": "<4.4",
              "impact": "Quadlet did not exist, so the `~/.config/containers/systemd` and `/etc/containers/systemd` directories are not read and no unit files are listed on the engines screen"
            },
            {
              "range": "<4.7",
              "impact": "`podman compose` does not exist, so a Compose project is listed and grouped but cannot be driven from here. Before 4.7 a project was run by calling podman-compose directly, which takes different arguments"
            },
            {
              "range": "<5.0",
              "impact": "`podman update --restart` does not exist — `podman update` arrived in 4.3 carrying only the resource limits — so the restart policy key refuses with that reason instead of running a command Podman would reject"
            },
            {
              "range": ">=4.0",
              "impact": "rootless and root are two separate sets of containers, not two views of one. Both are listed when both answer; root's are read through `sudo -n`, and where that needs a password the screen says the system scope was not read"
            },
            {
              "range": ">=4.0",
              "impact": "an infra container — the pause process holding a pod's namespaces open — is dropped from the list. It runs nothing anyone wrote, and showing one per pod would be an unexplained extra row"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-containers",
      "page": "https://tui.tools/tools/tui-containers/",
      "changelog": "https://github.com/tui-tools/tui-containers/releases/tag/v0.2.1"
    },
    {
      "name": "tui-cron",
      "package": "tui-cron",
      "binary": "tui-cron",
      "tagline": "Every scheduled job on one screen, systemd timers and cron together",
      "description": "A terminal UI for everything scheduled on a machine. systemd timers and cron are two schedulers with two syntaxes and two ways of reporting what happened; tui-cron puts them on one list — the name, the schedule as written, the next run, the last run and how it went.\n\nNext to every expression is what it means, in English. `17 3 * * 1` and `*-*-01..07 12:00:00` are both precise and neither is readable, and the mistakes people make with them are mistakes of reading. The reading comes from the tool's own describer, and the detail screen adds the next five runs as `systemd-analyze calendar` computes them.\n\nChanges are previewed. A timer's schedule goes to a drop-in, checked with `systemd-analyze calendar` first; the unit file is never rewritten. A crontab is replaced through `crontab <file>`, cron's own interface, with the diff on screen; as root, into another account's table or /etc/cron.d. A cron line can become a timer, written but not enabled. A timer this tool wrote says so in its files, and only those can be re-pointed or deleted.\n\nRun `tui-cron --demo` to drive a sample machine: every key works, every command is built and previewed for real, and nothing touches your system.",
      "category": "scheduling",
      "icon": "https://tui.tools/tools/tui-cron/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-cron/tui-cron-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:59:43Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "systemd",
        "cron"
      ],
      "compat": {
        "systemd": {
          "binary": "systemctl",
          "minimum": "245",
          "tested": [
            "255",
            "259",
            "261"
          ],
          "features": [
            {
              "name": "timers-json",
              "since": "250"
            }
          ],
          "notes": [
            {
              "range": "<250",
              "impact": "`systemctl list-timers --output=json` does not exist, so the timers are enumerated from `systemctl list-units --type=timer` instead; nothing is lost, because every column shown is read from `systemctl show` either way"
            },
            {
              "range": ">=245",
              "impact": "`systemd-analyze calendar --iterations` is what computes the next runs and validates a new OnCalendar. It arrived in 242, below this minimum, so it is not gated: on every version this tool supports it is simply there"
            },
            {
              "range": ">=245",
              "impact": "a timer in your own manager is read through `systemctl --user`, which needs a user bus; over a serial console or a bare `sudo -i` shell there is none, and the tool says the user timers were not read rather than that there are none"
            }
          ]
        },
        "cron": {
          "binary": "crontab",
          "minimum": null,
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": ">=1",
              "impact": "cron declares no version command. cronie answers `crontab -V` with \"cronie 1.7.2\"; Debian's vixie cron has no such flag and no sibling program that prints one, so a version would appear on Fedora and be blank on Ubuntu. The header shows cron without a number"
            },
            {
              "range": ">=1",
              "impact": "there is no portable way to have cron check a table: cronie ships `crontab -T`, Debian's cron ships nothing equivalent. A line is therefore parsed by this tool before it is written, with the same check on every machine"
            },
            {
              "range": ">=1",
              "impact": "cron records that a command started, and cronie also that it returned; neither records an exit status anywhere. A cron job's last result is therefore \"it ran\" at best, and the screen says so rather than implying it worked"
            },
            {
              "range": ">=1",
              "impact": "a machine may have no cron at all — Omarchy Server is one — and that is a normal machine, not a failure: the timers are listed, and cron reports itself as absent with the reason"
            },
            {
              "range": ">=1",
              "impact": "a machine with no cron may still carry run-parts scripts: Omarchy Server ships /etc/cron.hourly/snapper and nothing that walks the directory. They are listed anyway, and each row says nothing runs it rather than showing a file that looks scheduled as active"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-cron",
      "page": "https://tui.tools/tools/tui-cron/",
      "changelog": "https://github.com/tui-tools/tui-cron/releases/tag/v0.2.1"
    },
    {
      "name": "tui-dc",
      "package": "tui-dc",
      "binary": "tui-dc",
      "tagline": "A Samba Active Directory domain, administered from the terminal",
      "description": "A terminal UI over `samba-tool` for a machine that is a Samba Active Directory domain controller. It opens read-only: the domain's name and functional levels, the role this host plays in it, the password policy, the accounts, the groups, the machine accounts, the domain's own DNS zone, and the state of replication per naming context.\n\nEvery change is one `samba-tool` command, shown in full and confirmed before it runs — and no password is ever an argument. samba-tool warns about that itself, because a command line is visible in `ps` to every user on the machine, so creating an account and resetting a password ask samba-tool for a random password and show what it printed — and provisioning omits `--adminpass` so samba-tool generates the Administrator password itself and prints it exactly once.\n\nOn a machine that has samba-tool and no domain, a wizard provisions one: realm, NetBIOS name, DNS backend, optional forwarder, the realm typed back as a second confirmation, then the usual previewed command. It does not join or demote a domain: both touch a trust relationship with another controller, and are worth reading in a shell where they can be checked twice.",
      "category": "users",
      "icon": "https://tui.tools/tools/tui-dc/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-dc/tui-dc-domain.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:44:36Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "samba"
      ],
      "compat": {
        "samba": {
          "binary": "samba-tool",
          "minimum": "4.13",
          "tested": [],
          "features": [
            {
              "name": "computer-subcommand",
              "since": "4.8"
            }
          ],
          "notes": [
            {
              "range": "<4.8",
              "impact": "`samba-tool computer` does not exist, so the computers screen is empty"
            },
            {
              "range": "<4.13",
              "impact": "untested: the output of `domain info`, `dns query` and `drs showrepl` has changed shape across releases and the parsers are only checked against 4.19 and 4.22"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-dc",
      "page": "https://tui.tools/tools/tui-dc/",
      "changelog": "https://github.com/tui-tools/tui-dc/releases/tag/v0.2.1"
    },
    {
      "name": "tui-disk",
      "package": "tui-disk",
      "binary": "tui-disk",
      "tagline": "Disks, mounts, fstab, btrfs and SMART, previewed before every change",
      "description": "A terminal UI for the machine's storage. Five screens over one read: the block devices with their filesystems and usage bars, the mount table crossed against `/etc/fstab`, the btrfs filesystems in full, the SMART health of every drive, and what `df` says about free space.\n\nThe mounts screen is the one that earns its keep. It shows what is mounted now next to what fstab says will be mounted next boot, and marks every row where those disagree: a filesystem mounted by hand that will vanish on reboot, an fstab entry that failed to mount and nobody noticed, and options fstab asks for that are not in effect.\n\nChanges are previewed. Mounting and unmounting an entry, starting or cancelling a btrfs scrub or balance, starting a drive self-test, and editing `/etc/fstab` — which is validated with `findmnt --verify` against the staged file BEFORE the dialog opens, shown as a unified diff, then installed and applied with `systemctl daemon-reload` — all carry the exact command line.\n\nRun `tui-disk --demo` to drive a sample machine: an NVMe root on btrfs, a spinning disk with two reallocated sectors, a USB stick mounted by hand and one fstab mismatch. Nothing touches your system.",
      "category": "storage",
      "icon": "https://tui.tools/tools/tui-disk/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-disk/tui-disk-devices.png",
      "version": "0.1.3",
      "released": "2026-09-02T17:05:38Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "util-linux",
        "btrfs-progs",
        "smartmontools"
      ],
      "compat": {
        "util-linux": {
          "binary": "lsblk",
          "minimum": "2.34",
          "tested": [
            "2.39.3",
            "2.41.5",
            "2.42.2"
          ],
          "features": [
            {
              "name": "mountpoints",
              "since": "2.37"
            }
          ],
          "notes": [
            {
              "range": "<2.37",
              "impact": "lsblk has no `MOUNTPOINTS` column, so the single-valued `MOUNTPOINT` is asked for instead; a device mounted at several paths reports only the first one. lsblk fails the whole call on a column name it does not know, so this is a gate rather than a fallback"
            },
            {
              "range": ">=2.34",
              "impact": "`blkid` reads /dev directly and answers an unprivileged caller with an empty body and exit 0, so the fstab editor's device picker escalates with `sudo -n` and falls back to the UUIDs lsblk already reported when it cannot"
            }
          ]
        },
        "btrfs-progs": {
          "binary": "btrfs",
          "minimum": "5.10",
          "tested": [
            "6.6.3",
            "6.19.1",
            "7.1"
          ],
          "features": [
            {
              "name": "json-output",
              "since": "5.15"
            }
          ],
          "notes": [
            {
              "range": ">=5.10",
              "impact": "`--format json` is supported per command, not globally: `device stats` and `filesystem df` emit JSON, while `filesystem usage`, `subvolume list`, `scrub status` and `balance status` refuse it through 6.19 at least, so those four are read from their text output"
            },
            {
              "range": ">=5.10",
              "impact": "`subvolume list`, `qgroup show` and `balance status` refuse an unprivileged caller with \"Operation not permitted\"; they are retried with `sudo -n`, and a machine that cannot escalate shows those sections empty with a note saying so"
            },
            {
              "range": "<5.15",
              "impact": "no `--format json` at all, so `device stats` is read from its text output too"
            }
          ]
        },
        "smartmontools": {
          "binary": "smartctl",
          "minimum": "7.0",
          "tested": [],
          "features": [
            {
              "name": "json-output",
              "since": "7.0"
            }
          ],
          "notes": [
            {
              "range": "<7.0",
              "impact": "`smartctl --json` does not exist, and the health of every drive is reported as unknown rather than scraped out of a table meant for human eyes"
            },
            {
              "range": ">=7.0",
              "impact": "reading SMART needs the raw device, so it escalates with `sudo -n`; a drive behind a USB bridge that passes no SMART through, and a virtual disk that has no firmware, both come back unknown with the reason smartctl gave"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-disk",
      "page": "https://tui.tools/tools/tui-disk/",
      "changelog": "https://github.com/tui-tools/tui-disk/releases/tag/v0.1.3"
    },
    {
      "name": "tui-firewall",
      "package": "tui-firewall",
      "binary": "tui-firewall",
      "tagline": "ufw, firewalld and nftables, with every change previewed before it runs",
      "description": "A terminal UI for the Linux firewall, driving **ufw**, **firewalld** and **nftables** directly. It shows the rules you actually have — action, ports, source, destination, services, rich rules, IPv6 and forwarding included — alongside the default policies or chain policies and the logging level.\n\nThe UI never builds a command line. It talks to a backend behind an interface, which returns a firewall-neutral model and produces command values; the confirm dialog shows those values and hands the same ones to the runner, so what you read is what executes.\n\nOn a machine neither ufw nor firewalld manages it reads `nft -j list ruleset` and drives nft itself: filter chains by hook, a NAT view for masquerading and port forwards, and named sets as aliases with the number of rules using each. It writes only to a base chain whose policy nft reported, or to its own table, and says why when it will not.\n\nRun `tui-firewall --demo` for an in-memory ufw, `--demo=firewalld` for firewalld, or `--demo=nftables` for a sample router ruleset: every key works, every command is built and previewed for real, and nothing touches your system.",
      "category": "firewall",
      "icon": "https://tui.tools/tools/tui-firewall/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-firewall/tui-firewall-main.png",
      "version": "0.4.1",
      "released": "2026-09-02T16:41:58Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "ufw",
        "firewalld",
        "nftables"
      ],
      "compat": {
        "ufw": {
          "binary": "ufw",
          "minimum": "0.36",
          "tested": [
            "0.36.2"
          ],
          "features": [
            {
              "name": "rule-comments",
              "since": "0.35"
            }
          ],
          "notes": [
            {
              "range": "<0.36",
              "impact": "`ufw status numbered` has no app profile column, so a rule added from a profile is shown by its ports and cannot be edited as a profile"
            },
            {
              "range": "0.36.x",
              "impact": "`status numbered` indexes IPv4 and IPv6 halves of one rule separately, so deleting by number renumbers the rest and the list is re-read after every delete"
            }
          ]
        },
        "firewalld": {
          "binary": "firewall-cmd",
          "minimum": "0.9",
          "tested": [
            "2.4.4"
          ],
          "features": [],
          "notes": [
            {
              "range": "==2.0.0",
              "impact": "`--permanent --list-all-zones` prints the same settings for every zone on this release (firewalld#1152), so the permanent half of the runtime/permanent comparison is not trustworthy; 2.0.1 fixed it"
            },
            {
              "range": ">=2.2",
              "impact": "firewalld removed the lockdown feature, so no lockdown state is shown"
            }
          ]
        },
        "nftables": {
          "binary": "nft",
          "minimum": "0.9",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": "<0.9",
              "impact": "`nft -j` does not exist yet, so there is no ruleset to read in any form this tool will parse. 0.9.0 is where nft grew JSON output and stamped it json_schema_version 1, the schema this backend reads, and that is the whole reason the minimum is 0.9"
            },
            {
              "range": ">=0.9",
              "impact": "`nft -j list ruleset` carries rule handles whether or not `-a` is given, unlike the human output. A handle is how a rule is deleted, because positions shift the moment anything is inserted above them"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-firewall",
      "page": "https://tui.tools/tools/tui-firewall/",
      "changelog": "https://github.com/tui-tools/tui-firewall/releases/tag/v0.4.1"
    },
    {
      "name": "tui-logs",
      "package": "tui-logs",
      "binary": "tui-logs",
      "tagline": "The journal on one screen: narrow it, follow it, and shrink it safely",
      "description": "A terminal UI for the systemd journal. Every filter it offers is a journalctl argument — unit, priority, boot, time window, `--grep`, kernel only, the user journal — and the command line they add up to is on the status bar, so what is on screen is always something you could have typed.\n\nPress enter on a line and you get the record rather than the sentence: the pid, the uid, the executable and its command line, the source file and line for a program that logs through sd-journal, the MESSAGE_ID named where it is a well-known one, and every other field it carried. There is no clipboard, so the screen shows the `journalctl --cursor` invocation that prints that entry instead.\n\nA second screen counts the window: errors, the noisiest units, a sparkline. A third says what the journal costs and what journald.conf resolves to — and changes it: one key sets SystemMaxUse, MaxRetentionSec and Storage in a drop-in of its own, shown as a diff first.\n\nHousekeeping is four more commands, each previewed and confirmed: vacuum by size or age, rotate, verify.\n\nRun `tui-logs --demo` to drive a sample journal: every key works, every command is built and previewed for real, nothing is touched.",
      "category": "logs",
      "icon": "https://tui.tools/tools/tui-logs/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-logs/tui-logs-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:50:52Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "journald"
      ],
      "compat": {
        "journald": {
          "binary": "journalctl",
          "minimum": "239",
          "tested": [
            "255",
            "259",
            "261"
          ],
          "features": [
            {
              "name": "list-boots-json",
              "since": "252"
            }
          ],
          "notes": [
            {
              "range": ">=239",
              "impact": "the version comes from `journalctl --version`, whose first line is `systemd 257 (257.13-1.fc42)`; the first number is the release and the package version after it is ignored"
            },
            {
              "range": ">=239",
              "impact": "only one thing here is version-gated. `--grep` (237), `--after-cursor` (206) and `systemd-analyze cat-config` (239) all exist at or below the minimum, so they are never checked for"
            },
            {
              "range": "<252",
              "impact": "`journalctl --list-boots` cannot print JSON, so the boot list is parsed from its table instead; the picker works either way and nothing else changes"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-logs",
      "page": "https://tui.tools/tools/tui-logs/",
      "changelog": "https://github.com/tui-tools/tui-logs/releases/tag/v0.2.1"
    },
    {
      "name": "tui-network",
      "package": "tui-network",
      "binary": "tui-network",
      "tagline": "Links, addresses, routes and DNS, with every change previewed before it runs",
      "description": "A terminal UI for the machine's network. It shows the links you have — state, type, addresses, gateway, MAC, MTU and driver — with the routing table, what systemd-resolved uses for DNS, and the `.network` file behind each link.\n\nOpen a link and you get the whole picture in one screen: its addresses and where each came from, its routes, DNS servers and search domains, DHCP lease, the `.network` file that configures it, and what systemd-networkd said about it in the journal.\n\nChanges are previewed. Bringing a link up or down, reconfiguring it, renewing a lease, setting DNS at runtime, and editing a `.network` file — a unified diff, then an install and a reload — all go through a confirm dialog carrying the exact command line. A link systemd-networkd does not manage is shown read-only with the reason.\n\nThe `D` screen reads the router's DHCP server — dnsmasq, Kea or networkd's own `[DHCPServer]` — with its pools, leases and reservations, `w` switches the default route or fails over, and `V` builds a VLAN or a bridge as a `.netdev` unit.\n\nRun `tui-network --demo` to drive a sample machine: every key works, every command is previewed for real, and nothing touches your system.",
      "category": "network",
      "icon": "https://tui.tools/tools/tui-network/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-network/tui-network-main.png",
      "version": "0.3.1",
      "released": "2026-09-02T16:38:30Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "systemd-networkd",
        "dnsmasq",
        "kea"
      ],
      "compat": {
        "systemd-networkd": {
          "binary": "networkctl",
          "minimum": "245",
          "tested": [
            "255",
            "257",
            "259",
            "261"
          ],
          "features": [
            {
              "name": "json-status",
              "since": "249"
            },
            {
              "name": "link-up-down",
              "since": "249"
            },
            {
              "name": "dhcp-server-leases",
              "since": "246"
            },
            {
              "name": "dhcp-static-lease",
              "since": "249"
            }
          ],
          "notes": [
            {
              "range": "<249",
              "impact": "`networkctl --json` does not exist, so the columns of `networkctl list` and the `Key: value` block of `networkctl status` are parsed instead; addresses carry no prefix length there, and the DHCP lease clock is not reported at all"
            },
            {
              "range": "<249",
              "impact": "`networkctl up` and `down` do not exist; the keys are dropped from the hint bar and reconfigure is offered in their place"
            },
            {
              "range": ">=245",
              "impact": "no released systemd emits JSON from `resolvectl status`, so DNS servers and search domains are read from the text output of `resolvectl dns` and `resolvectl domain`"
            },
            {
              "range": ">=245",
              "impact": "the Gateways screen (w) reads the default routes from `ip -j route` (multipath expanded per next hop) and reachability from `ip -j route get`; the default-route switch and failover run `ip route replace` live, and a managed link also gets a persistent `[Route]` drop-in reloaded with `networkctl`"
            },
            {
              "range": ">=245",
              "impact": "VLANs and bridges (V) are written as a .netdev unit under /etc/systemd/network plus a VLAN= or Bridge= line in each member's .network file, previewed as one multi-file diff, installed with `install -m 644` and applied with a single `networkctl reload`; X removes a unit tui-network wrote with `rm -f`"
            },
            {
              "range": ">=245",
              "impact": "the DHCP screen (D) also reads systemd-networkd's own DHCP server, the one a `[DHCPServer]` section in a .network unit turns on: the pool derived from `Address=` with `PoolOffset=`/`PoolSize=`, the lease time, the advertised DNS, NTP, router and domain, and the static leases"
            },
            {
              "range": ">=245",
              "impact": "a change to that server is written to a drop-in of the tool's own (`<unit>.network.d/50-tui-network-dhcp.conf`), rendered whole and applied with `networkctl reload`; the unit itself is never rewritten, and each advertised list is cleared before it is set because a drop-in's `DNS=` adds to the unit's"
            },
            {
              "range": ">=245",
              "impact": "`[DHCPServer]` has no key for the domain (`EmitDomains=` belongs to `[IPv6SendRA]`), so tui-network emits it as the raw option it is, `SendOption=15:string:<domain>`"
            },
            {
              "range": "<246",
              "impact": "`networkctl status` does not print the \"Offered DHCP leases\" list (systemd 246 added it), so a networkd DHCP server shows its pool and its static leases but no live ones"
            },
            {
              "range": "<249",
              "impact": "`[DHCPServerStaticLease]` does not exist (systemd 249 added it), so a static lease written on an older systemd is read back by the screen but ignored by the server"
            }
          ]
        },
        "dnsmasq": {
          "binary": "dnsmasq",
          "minimum": "2.80",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": ">=2.80",
              "impact": "the DHCP screen reads the lease file (/var/lib/misc/dnsmasq.leases), the `dhcp-range` pools and `dhcp-host` reservations from the configuration, and dnsmasq serves DNS and DHCP from one process, so the resolver view on the links screen is only what systemd-resolved reports"
            },
            {
              "range": ">=2.80",
              "impact": "reservations tui-network adds are written to a drop-in of its own (/etc/dnsmasq.d/tui-network.conf) and applied with `systemctl reload dnsmasq`; a pool range change needs `systemctl restart dnsmasq`, which briefly interrupts DNS and DHCP"
            },
            {
              "range": ">=2.80",
              "impact": "Advertised DHCP options (DNS, gateway, domain) and upstream server= forwarders are edited through the tool's own drop-in (/etc/dnsmasq.d/50-tui-network.conf), regenerated whole; lines the tool does not own are never touched. Applying restarts dnsmasq."
            }
          ]
        },
        "kea": {
          "binary": "kea-dhcp4",
          "minimum": "2.0.0",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": ">=2.0.0",
              "impact": "the DHCP screen reads Kea's pools and host reservations from its JSON config (/etc/kea/kea-dhcp4.conf, // and /* */ comments stripped) and its leases from the memfile CSV (/var/lib/kea/kea-leases4.csv); Kea is read-only here, so the add, remove and pool-range keys are not offered"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-network",
      "page": "https://tui.tools/tools/tui-network/",
      "changelog": "https://github.com/tui-tools/tui-network/releases/tag/v0.3.1"
    },
    {
      "name": "tui-router",
      "package": "tui-router",
      "binary": "tui-router",
      "tagline": "Every part of the router on one screen",
      "description": "The read-only cockpit for a Linux router. One screen, one card per area — interfaces and their WAN/LAN roles, the firewall's posture, live traffic per interface, the DHCP server and its leases, the WireGuard peers — read from cheap system probes and refreshed in place.\n\nIt changes nothing itself. Press ENTER on a card and tui-router suspends, hands the terminal to the tool that manages that area — tui-firewall, tui-network, tui-traffic, tui-vpn — and resumes when you leave it, the same handoff the family launcher uses. The overview is here; the change happens in the tool the card opens, behind that tool's own preview and confirm.\n\nIt also carries what no per-area tool can: `tui-router export` writes one integrity-checked .tuiback artifact with the router's identity — the WAN/LAN roles, the networkd units, the forwarding and resolver drop-ins, DHCP/DNS, the WireGuard configs with their keys stripped, the saved firewall ruleset and the account names. `tui-router restore` puts it back behind a diff, a preview of every reload command, a typed confirmation and an atomic apply that rolls back unless you confirm you still have access (`--keep` skips that window for scripted restores).",
      "category": "network",
      "icon": "https://tui.tools/tools/tui-router/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-router/tui-router-main.png",
      "version": "0.3.2",
      "released": "2026-09-02T18:13:23Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "iproute2",
        "nftables",
        "wireguard-tools"
      ],
      "compat": {
        "iproute2": {
          "binary": "ip",
          "minimum": "5.0",
          "tested": [],
          "features": [
            {
              "name": "json-output",
              "since": "4.13"
            }
          ],
          "notes": [
            {
              "range": "<4.13",
              "impact": "`ip -j` (JSON output) is missing, so the interface and route cards cannot be read from this version"
            }
          ]
        },
        "nftables": {
          "binary": "nft",
          "minimum": "0.9",
          "tested": [],
          "features": [],
          "notes": []
        },
        "wireguard-tools": {
          "binary": "wg",
          "minimum": "1.0",
          "tested": [],
          "features": [],
          "notes": []
        }
      },
      "repo": "https://github.com/tui-tools/tui-router",
      "page": "https://tui.tools/tools/tui-router/",
      "changelog": "https://github.com/tui-tools/tui-router/releases/tag/v0.3.2"
    },
    {
      "name": "tui-samba",
      "package": "tui-samba",
      "binary": "tui-samba",
      "tagline": "Samba shares, accounts and live connections, with every change previewed",
      "description": "A terminal UI for a Samba file server: the shares it exports, the accounts that can reach them, and who is connected right now.\n\nThe shares are listed worst first. A path that does not exist, a directory somebody left at mode 0777, a writable share open to guests — each one is a sentence on the row rather than something to work out from `testparm` and a `stat`. The configuration comes from `testparm -s` — what the server resolved rather than what anybody typed — and every share carries the mode, the owner and the SELinux label of the directory behind it.\n\nA change goes through Samba's own parser before it goes anywhere near /etc. A share is edited in a guided form, staged, read back with `testparm -s` and shown as a diff — and only then confirmed. It is installed with `install -m 644` and picked up with a reload that disconnects nobody. Removing a share takes away the drop-in and the one `include` line that reached it, never the directory it exported.\n\nSamba as an Active Directory domain controller is deliberately out of scope.\n\nRun `tui-samba --demo` to drive a sample server: every key works, every command is built and previewed for real, and nothing touches your system.",
      "category": "file-sharing",
      "icon": "https://tui.tools/tools/tui-samba/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-samba/tui-samba-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:56:47Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "samba"
      ],
      "compat": {
        "samba": {
          "binary": "smbd",
          "minimum": "4.11",
          "tested": [
            "4.24.6"
          ],
          "features": [
            {
              "name": "status-json",
              "since": "4.17"
            }
          ],
          "notes": [
            {
              "range": "<4.17",
              "impact": "`smbstatus --json` does not exist, so the connections are read from the text output; the sessions, the shares in use and the open files are all there, and the per-session encryption is whatever those columns carry"
            },
            {
              "range": ">=4.17",
              "impact": "`smbstatus --json` is used, so a session's dialect, signing and encryption come from Samba's own document instead of from columns separated by runs of spaces a value may also contain"
            },
            {
              "range": ">=4.11",
              "impact": "SMB1 is off by default from this release, so a `server min protocol` below SMB2 is something somebody set on purpose — which is why tui-samba raises it and says nothing about a server on the default"
            },
            {
              "range": ">=4.11",
              "impact": "`testparm --show-all-parameters` is not used at all: it prints every parameter Samba has rather than this server's, so the effective configuration comes from `testparm -s`"
            },
            {
              "range": ">=4.11",
              "impact": "a machine may have no Samba at all, and that is a normal machine rather than a failure: the tool says so on its first screen and `--check` reports it as `\"installed\": false` and exits 0"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-samba",
      "page": "https://tui.tools/tools/tui-samba/",
      "changelog": "https://github.com/tui-tools/tui-samba/releases/tag/v0.2.1"
    },
    {
      "name": "tui-secure",
      "package": "tui-secure",
      "binary": "tui-secure",
      "tagline": "The machine's security posture, with the command behind every verdict",
      "description": "A terminal UI for the security posture of one Linux machine. Eight probes — Secure Boot, the MAC layer, the firewall, sshd, pending updates, accounts, kernel hardening and listening ports — each answered `ok`, `warn`, `bad` or `unknown`, with a score across the lot.\n\nEvery verdict shows its work. Open a probe and you get the exact command that was run, the line it judged, the full raw output underneath, and a fix that names either the sibling tool that owns the change (`tui-firewall`, `tui-update`) or the command to run yourself.\n\nIt reads first and asks second. Nothing escalates beyond `sudo -n`, which never prompts, so a probe that needs root it cannot get degrades to `unknown` with the reason instead of hanging on a password. The fixes it offers are the one-liners it can preview in full: turning on ufw, firewalld or nftables, setting one kernel key or one sshd keyword through a drop-in, enabling the update timer, stopping the service behind an open port. A change that would leave nothing able to log in over ssh is refused.\n\nRun `tui-secure --demo` to drive a sample machine: every key works, every command is built and previewed for real, and nothing touches your system.",
      "category": "security",
      "icon": "https://tui.tools/tools/tui-secure/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-secure/tui-secure-main.png",
      "version": "0.2.2",
      "released": "2026-09-02T16:48:20Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "systemd",
        "openssh",
        "ufw",
        "firewalld",
        "sbctl"
      ],
      "compat": {
        "systemd": {
          "binary": "systemctl",
          "minimum": "245",
          "tested": [
            "255",
            "257",
            "259",
            "261"
          ],
          "features": [
            {
              "name": "journal-grep",
              "since": "246"
            }
          ],
          "notes": [
            {
              "range": "<246",
              "impact": "`journalctl --grep` does not exist, so the denial and failed-login counts are read by filtering the journal here instead of in journalctl, which is slower on a large journal"
            }
          ]
        },
        "openssh": {
          "binary": "sshd",
          "minimum": "8.2",
          "tested": [
            "9.6",
            "9.9",
            "10.2",
            "10.5"
          ],
          "features": [
            {
              "name": "sshd-test-config",
              "since": "8.2"
            }
          ],
          "notes": [
            {
              "range": "<8.2",
              "impact": "`sshd -T` predates several of the keywords this probe reads, so the settings come from parsing sshd_config and its drop-ins instead, which does not resolve Match blocks"
            }
          ]
        },
        "ufw": {
          "binary": "ufw",
          "minimum": "0.36",
          "tested": [
            "0.36.2"
          ],
          "features": [],
          "notes": [
            {
              "range": "<0.36",
              "impact": "`ufw status verbose` prints no default policy line, so the incoming and outgoing policies are reported as unknown and only the active state is judged"
            }
          ]
        },
        "firewalld": {
          "binary": "firewall-cmd",
          "minimum": "0.9",
          "tested": [
            "2.3.2",
            "2.4.4"
          ],
          "features": [],
          "notes": []
        },
        "sbctl": {
          "binary": "sbctl",
          "minimum": null,
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": ">=0.1",
              "impact": "sbctl is optional: without it the Secure Boot probe reports what `bootctl status` knows and says nothing about enrolled keys or signed files"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-secure",
      "page": "https://tui.tools/tools/tui-secure/",
      "changelog": "https://github.com/tui-tools/tui-secure/releases/tag/v0.2.2"
    },
    {
      "name": "tui-snapper",
      "package": "tui-snapper",
      "binary": "tui-snapper",
      "tagline": "btrfs snapshots, managed by snapper, from the terminal",
      "description": "A terminal UI for the btrfs snapshots `snapper` manages. It lists every config and its history — pre/post pairs from package upgrades next to the hourly timeline snapshots — compares any two of them, and shows what changed file by file, with a read-only diff for anything you want to look at closely.\n\nIt creates, deletes, relabels and cleans up snapshots too, and puts individual files back with `undochange` without touching the rest of the filesystem. The configs themselves are authored from the same screen: create one for a subvolume, edit its retention limits, delete it. Every one of those is shown as the exact `snapper` command line and confirmed before it runs; starting the tool only reads.\n\nRollback is the one thing that genuinely differs between machines, so it is detected rather than assumed: where `snapper rollback` is the mechanism it is offered with a full warning, and on an Omarchy or limine-snapper-sync layout the boot menu entries are read from `/boot/limine.conf` and shown read-only, because that is where the rollback really happens.\n\n`--demo` runs the whole UI against a sample history, so you can try it without a btrfs filesystem to risk.",
      "category": "snapshots",
      "icon": "https://tui.tools/tools/tui-snapper/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-snapper/tui-snapper-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:51:07Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "snapper"
      ],
      "compat": {
        "snapper": {
          "binary": "snapper",
          "minimum": "0.8.6",
          "tested": [
            "0.10.6",
            "0.13.0",
            "0.13.1"
          ],
          "features": [
            {
              "name": "undochange-root-paths",
              "since": "0.10.6"
            }
          ],
          "notes": [
            {
              "range": "<0.8.6",
              "impact": "snapper has no `--jsonout`, and both the config list and the snapshot list are read as JSON, so nothing can be listed at all"
            },
            {
              "range": "<0.10.6",
              "impact": "`snapper undochange` cannot recreate a path that sits directly under the subvolume root, so putting back a deleted top-level file or directory fails with \"failed to create\""
            },
            {
              "range": "<0.10.7",
              "impact": "`snapper diff` is broken for LVM-based configs, so the file view comes back empty there even when the compare screen lists the change"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-snapper",
      "page": "https://tui.tools/tools/tui-snapper/",
      "changelog": "https://github.com/tui-tools/tui-snapper/releases/tag/v0.2.1"
    },
    {
      "name": "tui-ssh",
      "package": "tui-ssh",
      "binary": "tui-ssh",
      "tagline": "The SSH server as it really is, with every change previewed before it runs",
      "description": "A terminal UI for the machine's SSH server. It asks sshd what its configuration actually is — `sshd -T`, not what a file says — and shows the settings that decide who gets in first, each with a verdict and the file and line that set it.\n\nFive more screens complete the picture: who is logged in right now and a way to end a session; what the authentication log says over the last day or week; the host keys and their fingerprints; the unit and its sockets; and the local accounts with the keys that log into them, where one is added or removed.\n\nChanges go to a drop-in: tui-ssh never rewrites /etc/ssh/sshd_config. It writes 90-tui-ssh.conf, checks the staged file with `sshd -t -f` before asking anything, and shows the diff and both commands. sshd takes the first value it is given, so the include order is read too: if another file already sets the keyword earlier, the tool says so instead of writing a line that would be ignored. A keyword can also go inside a `Match` block, which the file keeps last: sshd reads what follows one as part of it.\n\nRun `tui-ssh --demo` to drive a sample server: every key works, every command is built and previewed for real, and nothing touches your system.",
      "category": "ssh",
      "icon": "https://tui.tools/tools/tui-ssh/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-ssh/tui-ssh-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:59:59Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "openssh"
      ],
      "compat": {
        "openssh": {
          "binary": "ssh",
          "minimum": "8.2",
          "tested": [
            "9.6",
            "10.2",
            "10.5"
          ],
          "features": [
            {
              "name": "include-dropins",
              "since": "8.2"
            },
            {
              "name": "kbd-interactive",
              "since": "8.7"
            }
          ],
          "notes": [
            {
              "range": ">=10.5",
              "impact": "`sshd -T` prints the canonical spelling of every keyword (`PermitRootLogin no`) where 10.2 and earlier lower-case it (`permitrootlogin no`); both forms are parsed to the canonical name, so the verdicts and the source lines are the same either way"
            },
            {
              "range": ">=8.2",
              "impact": "the version comes from `ssh -V`, which prints to standard error; `sshd -V` only exists from OpenSSH 9.6, so asking the server itself would report nothing over most of the supported range"
            },
            {
              "range": "<8.2",
              "impact": "`Include` does not exist, so /etc/ssh/sshd_config.d is never read; tui-ssh will not rewrite sshd_config itself, so the editor has nowhere to write and says so"
            },
            {
              "range": "<8.7",
              "impact": "`KbdInteractiveAuthentication` is still spelled `ChallengeResponseAuthentication`; both are read, and the old name is what gets written"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-ssh",
      "page": "https://tui.tools/tools/tui-ssh/",
      "changelog": "https://github.com/tui-tools/tui-ssh/releases/tag/v0.2.1"
    },
    {
      "name": "tui-systemd",
      "package": "tui-systemd",
      "binary": "tui-systemd",
      "tagline": "systemd units: what failed, why, and every action previewed",
      "description": "A terminal UI for systemd. It opens on what failed — failed units sort first, always — and puts the journal that explains why one key away. Timers show what is scheduled and when it last fired; the boot view shows what made the last boot slow.\n\nIt also writes. `c` shows the unit file as systemd assembles it. `E` opens a guided editor over a closed set of properties — Restart, RestartSec, MemoryMax, CPUQuota, Environment, Nice, and OnCalendar on a timer — and renders the whole drop-in at `/etc/systemd/system/<unit>.d/90-tui-systemd.conf`. `n` creates a service, or a timer and the service it starts, from a vetted template. Every write is staged in a temporary directory, read by `systemd-analyze verify` before you are asked anything, shown as a unified diff, and only then installed with `install -m 644` and a `daemon-reload`.\n\nEvery read works as an ordinary user, so the tool opens instantly and without a password; only a change escalates, and only behind a confirm dialog showing the exact command lines.\n\nRun `tui-systemd --demo` for a sample machine with a couple of interesting failures. Nothing touches your system.",
      "category": "systemd",
      "icon": "https://tui.tools/tools/tui-systemd/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-systemd/tui-systemd-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:47:53Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "systemd"
      ],
      "compat": {
        "systemd": {
          "binary": "systemctl",
          "minimum": "230",
          "tested": [
            "255",
            "259",
            "261"
          ],
          "features": [
            {
              "name": "timers",
              "since": "250"
            },
            {
              "name": "boot-blame",
              "since": "230"
            }
          ],
          "notes": [
            {
              "range": "<250",
              "impact": "`list-timers` has no JSON output, so the timers view is not offered: the text table cannot be parsed without mangling its timestamps"
            },
            {
              "range": "<245",
              "impact": "`list-units --plain` is absent on some builds, so the unit list falls back to the decorated output"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-systemd",
      "page": "https://tui.tools/tools/tui-systemd/",
      "changelog": "https://github.com/tui-tools/tui-systemd/releases/tag/v0.2.1"
    },
    {
      "name": "tui-template",
      "package": "tui-template",
      "binary": "tui-template",
      "tagline": "A working skeleton for a new tui-tools tool",
      "description": "The starting point for a new tool in the family. Press **Use this template**, rename it, replace one package, and you have a tool that looks and behaves like the rest.\n\nIt is not a pile of TODOs: it is a working tool. It lists the files in a directory and can update a file's timestamp, which is deliberately trivial — what matters is the shape around it, and that shape is already correct: the action table that generates the key map, the help screen and the confirm dialog; the backend interface with a real and a fake implementation; `--demo`; CI; and GoReleaser.\n\nThis `tool.json` is itself the example. It is a valid manifest, validated against [the schema](https://github.com/tui-tools/tui-kit/blob/main/schema/tool.schema.json) on every CI run, and the values are the ones you replace.",
      "category": "template",
      "icon": "https://tui.tools/tools/tui-template/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-template/tui-template-main.png",
      "version": null,
      "released": null,
      "unreleased": true,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "coreutils"
      ],
      "compat": {
        "coreutils": {
          "binary": "touch",
          "minimum": "8.0",
          "tested": [],
          "features": [
            {
              "name": "no-dereference",
              "since": "8.1"
            }
          ],
          "notes": [
            {
              "range": "<8.1",
              "impact": "`touch -h` is missing, so the timestamp of a symlink is set on its target instead of on the link"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-template",
      "page": "https://tui.tools/tools/tui-template/",
      "changelog": null
    },
    {
      "name": "tui-tools",
      "package": "tui-tools",
      "binary": "tui-tools",
      "tagline": "Every tui-tools tool in one place, installed through your package manager",
      "description": "The launcher of the [tui-tools](https://tui.tools) family. Every tool of the family is a card: what it does, whether this machine has it, which version, and whether the repository offers a newer one.\n\nA second group holds the companions: family packages that are not terminal UIs, and upstream projects rebuilt under the family provenance gate. The row says which repository the installed copy came from; `o` switches it to the family's build.\n\nInstall, update and remove go through the distribution's own package manager — `apt`, `dnf` or `pacman` — each previewed as the exact command line and confirmed before it runs. `enter` hands the terminal to an installed tool and takes it back on exit.\n\nThe catalog it reads only says which packages exist. It never decides what reaches the machine: your package manager verifies the signed repository index and the signed package, and every package name in it is validated twice before it can reach a command line. A snapshot of it is built into the binary, so the dashboard works offline.\n\n`s` sets the repository up, with the key pinned by fingerprint.\n\nRun `tui-tools --demo` for a sample machine where every key works and nothing is touched.",
      "category": "packages",
      "icon": "https://tui.tools/tools/tui-tools/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-tools/tui-tools-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T17:02:45Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "pacman",
        "apt",
        "dnf"
      ],
      "compat": {
        "pacman": {
          "binary": "pacman",
          "minimum": "6.0",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": ">=6.0",
              "impact": "Arch has no supported way to upgrade one package against a refreshed database without upgrading the machine with it, so an update here is `pacman -Syu` with the tool named, and the confirm dialog says so before it runs"
            },
            {
              "range": ">=6.0",
              "impact": "the repository setup writes a `[tui-tools]` section under /etc/pacman.d and adds one Include line to pacman.conf, so a later setup finds it again instead of appending a second block"
            },
            {
              "range": ">=6.0",
              "impact": "pacman records no repository for an installed package, so a companion's origin is inferred rather than read: a bare `[installed]` in `pacman -Sl tui-tools` means the machine has the version that repository offers, and `pacman -Si` names the repository whose version matches the installed one"
            }
          ]
        },
        "apt": {
          "binary": "apt",
          "minimum": "2.0",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": ">=2.0",
              "impact": "the available version comes from `apt-cache policy`, which reads the lists already on disk; a repository added a moment ago reports nothing until the refresh, which is the first previewed step of an install rather than something the read path does behind you"
            },
            {
              "range": ">=2.0",
              "impact": "a companion's origin is read off `apt-cache policy`: the version marked `***` is the installed one, and the first source under it that is not /var/lib/dpkg/status is where it came from. apt cannot install from a named repository, so a switch names the family's version instead"
            }
          ]
        },
        "dnf": {
          "binary": "dnf",
          "minimum": "4.0",
          "tested": [],
          "features": [
            {
              "name": "dnf5",
              "since": "5.0"
            }
          ],
          "notes": [
            {
              "range": ">=5.0",
              "impact": "`dnf --version` prints `dnf5 version 5.2.18.0`, where dnf4 prints a bare `4.24.0` on its first line; both are read by the same pattern, which keeps three components because a four-part version is not one the family schema records"
            },
            {
              "range": ">=4.0",
              "impact": "the available version comes from `dnf repoquery`, which answers from the cache on disk, so a machine whose metadata has expired reports what it last fetched rather than refreshing behind you"
            },
            {
              "range": ">=4.0",
              "impact": "dnf is the one manager that records where an installed package came from, so a companion's origin is read rather than inferred: `dnf repoquery --installed --qf '%{name}|%{from_repo}'` answers it on dnf4 and dnf5, and `@System` means it came from a file"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-tools",
      "page": "https://tui.tools/tools/tui-tools/",
      "changelog": "https://github.com/tui-tools/tui-tools/releases/tag/v0.2.1"
    },
    {
      "name": "tui-traffic",
      "package": "tui-traffic",
      "binary": "tui-traffic",
      "tagline": "What the network is doing right now, interface by interface",
      "description": "Realtime network metrics for one Linux machine, in the terminal. Three screens, refreshed every second, and every number on them comes from a file the kernel already exports.\n\n**Interfaces** is throughput per interface: RX and TX in bytes per second and packets per second, a sparkline over the last minute, the link state, and the errors and drops the counters have accumulated. It is `/proc/net/dev` read twice a second apart, which is what a rate actually is.\n\n**Connections** summarises the conntrack table: how many connections there are, what states they are in, split by protocol, and the busiest talkers by bytes when the kernel is counting them. Byte accounting (`net.netfilter.nf_conntrack_acct`) is off by default on most kernels, and when it is off this screen says so rather than inventing figures. On a machine with no conntrack at all it counts sockets instead and names the source it used.\n\n**Sockets** is what is listening and what is established, from `/proc/net/tcp`, `tcp6`, `udp` and `udp6`.\n\nIt changes nothing: no action key, no confirm dialog, and no privilege beyond the `sudo -n` that reading the conntrack table needs.",
      "category": "network",
      "icon": "https://tui.tools/tools/tui-traffic/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-traffic/tui-traffic-main.png",
      "version": "0.1.1",
      "released": "2026-09-02T16:44:59Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "conntrack"
      ],
      "compat": {
        "conntrack": {
          "binary": "conntrack",
          "minimum": "1.4.0",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": "<1.4.0",
              "impact": "`conntrack -L -o extended` prints a layout this parser does not read, so the connections screen falls back to /proc/net/nf_conntrack or to counting sockets, and says which it used"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-traffic",
      "page": "https://tui.tools/tools/tui-traffic/",
      "changelog": "https://github.com/tui-tools/tui-traffic/releases/tag/v0.1.1"
    },
    {
      "name": "tui-update",
      "package": "tui-update",
      "binary": "tui-update",
      "tagline": "Pending package updates, what they restart, and a snapshot first",
      "description": "A terminal UI for the updates waiting on your machine. It reads them from whichever package manager the machine runs — `pacman`, `apt` or `dnf` — and shows what a list of versions never tells you: what applying them will cost.\n\nEvery pending update is on one screen, with its current and new version, its repository, its size and its security flag where the manager publishes one, and the kernel and firmware sorted to the top.\n\n`enter` gives you the plan: the manager's own dry run, the restart classification — nothing, the services holding replaced code open, or a reboot with the reason — whether a `snapper` snapshot can be taken first, and the whole sequence in order.\n\n`U` applies it behind a confirm dialog carrying that sequence, streaming the output as it goes. It never reboots by itself: when the upgrade needs one it offers `R`, confirmed too.\n\nOn the plan, `m` cycles the upgrades the manager really has, security-only on dnf included, and `s` turns the snapshot pair off and on. On the list, `H` holds a package at its version, or lifts the hold.\n\nRun `tui-update --demo` to drive a sample machine: every key works, every command is previewed for real, and nothing touches your system.",
      "category": "packages",
      "icon": "https://tui.tools/tools/tui-update/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-update/tui-update-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T17:01:52Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "pacman",
        "apt",
        "dnf"
      ],
      "compat": {
        "pacman": {
          "binary": "pacman",
          "minimum": "6.0",
          "tested": [
            "7.1.0"
          ],
          "features": [],
          "notes": [
            {
              "range": ">=6.0",
              "impact": "pacman publishes no security metadata, so no update is ever marked as a security fix here; the column reads `n/a` rather than `no`"
            },
            {
              "range": ">=6.0",
              "impact": "there is no dry run that does not first synchronise the databases, which needs root, so the plan quotes the pending list instead of a simulated transaction"
            },
            {
              "range": ">=6.0",
              "impact": "`checkupdates` needs pacman-contrib *and* fakeroot, since it builds its private copy of the sync database under it; without either the pending list falls back to `pacman -Qu`, which is whatever the last `pacman -Sy` left on disk, and the screen says so"
            },
            {
              "range": ">=6.0",
              "impact": "on Omarchy Server the upgrade runs through `omarchy-server-update run --no-reboot`, and `omarchy-server-update-restart --dry-run` is what classifies the restarts; on plain Arch there is no classifier and the package names decide"
            }
          ]
        },
        "apt": {
          "binary": "apt",
          "minimum": "2.0",
          "tested": [
            "2.8.3"
          ],
          "features": [
            {
              "name": "solver3",
              "since": "2.9.3"
            }
          ],
          "notes": [
            {
              "range": ">=2.0",
              "impact": "a security update is one whose pocket ends in `-security`; apt publishes no advisory id, so that pocket name is the whole reference"
            },
            {
              "range": ">=2.0",
              "impact": "neither `apt list --upgradable` nor `apt-get -s upgrade` reports a per-package size, so the size column is not shown; the plan carries apt's own download and disk totals instead"
            },
            {
              "range": ">=2.0",
              "impact": "the services to restart come from `needrestart -b`, which reads other processes' memory maps and therefore needs root; without it the package names decide"
            },
            {
              "range": ">=2.0",
              "impact": "there is no upgrade that applies only the security updates; `unattended-upgrade` honours Unattended-Upgrade::Allowed-Origins, which on Debian includes the plain archive too and can reboot on its own, so the security-only mode is not offered here"
            },
            {
              "range": ">=2.0",
              "impact": "a package is held with `apt-mark hold`, the dpkg selection every apt front end honours; `apt-mark showhold` is the unprivileged read the pending list marks its rows from"
            }
          ]
        },
        "dnf": {
          "binary": "dnf",
          "minimum": "4.0",
          "tested": [
            "5.2.18",
            "5.4.1"
          ],
          "features": [
            {
              "name": "dnf5",
              "since": "5.0"
            }
          ],
          "notes": [
            {
              "range": ">=5.0",
              "impact": "`dnf --version` prints `dnf5 version 5.2.18.0`, where dnf4 prints a bare `4.24.0` on its first line; both are read by the same pattern, which keeps three components because a four-part version is not one the family schema records"
            },
            {
              "range": ">=5.0",
              "impact": "`dnf needs-restarting` refreshes the repository metadata before answering, which a read path must not do, so the standalone `needs-restarting` binary from dnf-plugins-core is used instead and its absence falls back to the package names"
            },
            {
              "range": ">=4.0",
              "impact": "`dnf check-update` exits 100 when there are updates, so its exit code is interpreted rather than treated as a failure; it also prints only the new version, and the installed one is read from rpm"
            },
            {
              "range": ">=4.0",
              "impact": "`dnf upgrade --security` narrows the same transaction to the advisories, so the security-only mode is the plain upgrade with one flag rather than a second code path; dependencies with no advisory of their own still come along"
            },
            {
              "range": ">=4.0",
              "impact": "holding a package needs the versionlock plugin, which a minimal install does not carry; it is detected before the key is offered, and the refusal names python3-dnf-plugin-versionlock, or dnf-plugins-extras-versionlock on RHEL and its rebuilds"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-update",
      "page": "https://tui.tools/tools/tui-update/",
      "changelog": "https://github.com/tui-tools/tui-update/releases/tag/v0.2.1"
    },
    {
      "name": "tui-users",
      "package": "tui-users",
      "binary": "tui-users",
      "tagline": "Accounts, groups, keys and sudo, with every change previewed before it runs",
      "description": "A terminal UI for the machine's local accounts. It lists every account with the facts that decide what it can do — uid, primary group, groups, shell, last login, whether the password is usable, locked or missing — and puts the ones worth a second look at the top, with the reason: a second account with uid 0, an empty password, a service account with a login shell.\n\nOpen an account and you get the whole picture: its aging policy in words rather than in days-since-1970, the authorized keys in its `~/.ssh` with their fingerprints, what `sudo` says it may run, and its open sessions. A second screen lists the groups with their real membership, primary members included; a third quotes every rule in `/etc/sudoers` and `/etc/sudoers.d`, marking the ones that run without a password.\n\nChanges are previewed: creating and deleting accounts and groups, locking an account, setting a password, granting or revoking sudo, editing group membership, the login shell, the expiry, and authorized keys. A password never reaches a command line — it goes to `chpasswd` on standard input.\n\nRun `tui-users --demo` to drive a sample machine: every key works and nothing touches your system.",
      "category": "users",
      "icon": "https://tui.tools/tools/tui-users/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-users/tui-users-main.png",
      "version": "0.2.1",
      "released": "2026-09-02T16:57:14Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "shadow-utils",
        "openssh"
      ],
      "compat": {
        "shadow-utils": {
          "binary": "useradd",
          "minimum": "4.8",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": ">=4.8",
              "impact": "shadow-utils prints no version from any of its programs: `useradd --version`, `chage --version` and every sibling answer \"unrecognized option\" on Arch, Debian and Fedora alike. This backend therefore declares no version command, and the header shows none for it"
            },
            {
              "range": ">=4.8",
              "impact": "the minimum above is informative, not enforced: it is the release whose flags this tool uses (`useradd -r -m -G`, `usermod -L -U -s`, `userdel -r`, `gpasswd -a -d`, `groupadd -g`, `groupdel`, `chage -E -M -l`), all far older than any supported distribution"
            },
            {
              "range": ">=4.8",
              "impact": "reading the lock state and the expiry of an account needs `getent shadow`, which needs root; an unprivileged run lists every account and says the password state is unknown rather than implying it is fine"
            }
          ]
        },
        "openssh": {
          "binary": "ssh-keygen",
          "minimum": "8.2",
          "tested": [
            "9.6",
            "10.2",
            "10.5"
          ],
          "features": [
            {
              "name": "security-keys",
              "since": "8.2"
            }
          ],
          "notes": [
            {
              "range": "<8.2",
              "impact": "`ssh-keygen -l` does not know the `sk-` key types, so a FIDO security key in an authorized_keys file cannot be fingerprinted and adding one is warned about: sshd would ignore the line it wrote"
            },
            {
              "range": "<6.8",
              "impact": "`ssh-keygen -l` prints MD5 fingerprints rather than SHA256, so the fingerprint column will not match what a modern `ssh-keygen -lf` shows elsewhere"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-users",
      "page": "https://tui.tools/tools/tui-users/",
      "changelog": "https://github.com/tui-tools/tui-users/releases/tag/v0.2.1"
    },
    {
      "name": "tui-vpn",
      "package": "tui-vpn",
      "binary": "tui-vpn",
      "tagline": "WireGuard and its control plane, from the terminal",
      "description": "WireGuard from the terminal, together with the self-hosted control plane that decides who is allowed onto it.\n\nThe status screen reads your interfaces straight from `wg show all dump`: peers, endpoints, handshakes, transfer, allowed IPs and keepalive. Creating an interface from zero, up/down, adding or removing a peer, saving the runtime config — every change is previewed as the exact command line and applied only after you confirm it. A private key is never shown, typed, or put on a command line.\n\nWith a [Headscale](https://headscale.net) control plane it also manages users, nodes and pre-auth keys, and configures identity itself: `S` writes `server_url` and `listen_addr`, `O` writes the whole `oidc` section of `/etc/headscale/config.yaml`, each previewing a minimal diff — only the lines that change. The OIDC client secret is typed masked, written mode 600 to its own file owned by the account the `headscale` unit runs as, referenced as `client_secret_path`, and never displayed again. Login stays where it belongs: the client's browser, against your IdP.\n\nRun it with `--demo` to see every screen with nothing installed.",
      "category": "network",
      "icon": "https://tui.tools/tools/tui-vpn/icon.svg",
      "screenshot": "https://tui.tools/tools/tui-vpn/tui-vpn-status.png",
      "version": "0.3.0",
      "released": "2026-09-02T17:55:54Z",
      "unreleased": false,
      "platforms": [
        "linux/amd64",
        "linux/arm64"
      ],
      "license": "MIT",
      "backends": [
        "wireguard-tools",
        "headscale"
      ],
      "compat": {
        "wireguard-tools": {
          "binary": "wg",
          "minimum": "1.0.20200513",
          "tested": [],
          "features": [],
          "notes": []
        },
        "headscale": {
          "binary": "headscale",
          "minimum": "0.22.0",
          "tested": [],
          "features": [],
          "notes": [
            {
              "range": "<0.23",
              "impact": "`preauthkeys list` requires a `--user`, so the pre-auth keys screen may be empty; users and nodes are unaffected"
            }
          ]
        }
      },
      "repo": "https://github.com/tui-tools/tui-vpn",
      "page": "https://tui.tools/tools/tui-vpn/",
      "changelog": "https://github.com/tui-tools/tui-vpn/releases/tag/v0.3.0"
    }
  ],
  "companions": [
    {
      "name": "headscale",
      "kind": "mirror",
      "summary": "Self-hosted Tailscale control server, rebuilt from the upstream source tag",
      "upstream": "https://github.com/juanfont/headscale",
      "upstreamVersion": "v0.29.3",
      "version": "0.29.3",
      "released": "2026-09-01",
      "packages": [
        "headscale"
      ],
      "repo": "https://github.com/tui-tools/headscale",
      "page": "https://tui.tools/#companions"
    }
  ]
}
