Skip to content

Repository files navigation

tui-tools

The family website: tui-tools.github.io.

A marketplace for the tui-tools family, built entirely from each tool's own tool.json. Nothing here is edited by hand to add a tool, change a version, or update a screenshot.

The marketplace grid

A tool's page

How it works

tui-tools/<tool>/tool.json  ─┐
GitHub releases API         ─┼─► scripts/build-catalog.mjs ─► src/data/catalog.json ─┬─► scripts/build-og.mjs ─► public/og/*.png ─┐
icons and screenshots       ─┘                                                     └───────────────── astro build ─────────────────┴─► Pages

scripts/build-catalog.mjs lists the public repositories in the organization and asks each one for a tool.json at the default branch's HEAD. A repository that has one is a tool; one that does not — tui-kit, .github, this repository — is simply not in the grid. For each tool it then collects:

  • the manifest: tagline, description, category, platforms, keys, install channels, security posture, maintainers, and the backends the tool drives with everything it knows about their versions;
  • the latest release: tag, date, every asset with its size, and the SHA-256 sums parsed out of that release's checksums.txt;
  • the release history, for the changelog list on the tool page;
  • the images the manifest points at — the icon and every screenshot — downloaded into public/tools/<name>/, so the site serves its own copies and loads nothing from an external host at runtime.

A tool with no release yet is kept and marked unreleased rather than hidden. tui-template is the standing example.

The schema those manifests are validated against lives in the kit: tui-kit/schema/tool.schema.json, documented in tui-kit/docs/tool-manifest.md.

Link previews

A link to this site is mostly shared into somewhere that draws a card — X, Slack, LinkedIn, Discord — and that card is the first look most people get. scripts/build-og.mjs draws one per page, 1200×630, into public/og/<slug>.png between the catalog and astro build:

  • home, install, security and kit get the family card: the brand mark, the wordmark, the page's own line, and the domain.
  • every tool gets a tool card: >_ tui-<name>, the tagline out of its tool.json, and its first screenshot fitted on the right. The screenshot is already on disk — the catalog downloaded it a step earlier — so nothing is fetched here, and a tool whose manifest has no screenshot falls back to its icon.

Base.astro takes an ogSlug prop and points og:image at the matching file, absolute, with twitter:card = summary_large_image. A page that names no slug gets the family card, which is what 404 does.

satori renders the layout to SVG and @resvg/resvg-js rasterises it; both are plain npm packages with prebuilt musl binaries, so the node:22-alpine build stage needs nothing installed. The two typefaces come from @fontsource/* in node_modules, not from a font CDN, so a build with no network still draws the same bytes.

Backend compatibility

A tool that drives someone else's program declares it in the manifest's backends[], and the site renders that block as the Compatibility section on the tool page: the binary, the minimum version the tool claims, the versions it has really been tested against, the features that need a given version, and the version-ranged caveats with what each one does to the user. The grid card carries the short form of the same fact — ufw ≥ 0.36 — because that is what decides whether the tool is worth opening. A tool that shells out to nothing declares no backends and neither surface shows anything.

tested is evidence rather than a claim: the versions come from compat/results.jsonl in the tool's own repository, which its smoke suite writes while running inside a tui-lab guest, and tui-kit/tools/compat-sync.py regenerates the manifest from. The running binary reads the same block to probe the backend at startup, so a version the site does not list is one the tool's header marks (untested). The whole mechanism is documented in tui-kit/docs/compatibility.md.

scripts/build-catalog.mjs copies the block into the catalog minus the fields only the running binary needs — versionRegex and searchPaths — since the probe is the tool's business, not the website's.

Adding a tool to the site

There is no step in this repository.

  1. Start from tui-template and build the tool.
  2. Fill in its tool.json — the template's copy is a valid example, and CI validates it against the schema on every push.
  3. Tag v0.1.0 and let the release ship.
  4. The site picks it up on its next hourly build. To not wait, run the workflow: gh workflow run publish.yml -R tui-tools/tui-tools.github.io.

Publishing

.github/workflows/publish.yml runs on:

Trigger Why
push to main a change to the site itself
schedule, hourly at :17 a release in another repository appears within the hour
workflow_dispatch rebuild on demand

It builds the catalog, runs astro build, and deploys dist/ with actions/deploy-pages. Pages is configured with GitHub Actions as its source; there is no gh-pages branch.

Why nothing wakes it

A tool's release could tell this repository to rebuild immediately, and for a while one did. Doing so needs a token with write access to this repository, held as a secret on every public tool repository — fourteen copies of a key to the site, to save at most an hour of staleness on a page nobody is watching change. No secrets outside this repository. The tools publish releases and the site comes and looks; when an hour is too long, run the workflow by hand:

gh workflow run publish.yml -R tui-tools/tui-tools.github.io

The custom domain (owner action)

This is the custom domain on Pages. To move the domain to a container host instead, see Cutover to Quave ONE below, which supersedes this.

The site is written so switching to tui.tools is two settings and no code:

  1. Set the repository variable SITE_DOMAIN to tui.tools (gh variable set SITE_DOMAIN --repo tui-tools/tui-tools.github.io --body tui.tools). The workflow then writes dist/CNAME and builds the canonical URLs against the new host.
  2. Add the DNS record: CNAME tui.tools → tui-tools.github.io. For an apex domain, use the four A records GitHub documents for Pages instead, and keep a CNAME for www.

Do both. Setting the variable without the DNS record leaves Pages serving a domain that does not resolve; adding the DNS record without the variable leaves Pages redirecting to tui-tools.github.io. Once DNS is live, turn on Enforce HTTPS in the repository's Pages settings.

Nothing else in the site hardcodes the host: every internal link is root-relative, which is why this is the organization site repository (tui-tools.github.io, served at /) rather than a project repository served under a path. The three places that need an absolute URL — the canonical link, sitemap-index.xml and robots.txt — all derive it from the same site value, so SITE_URL is the only knob.

Sitemap and robots

Both are generated at build time and neither is committed:

  • sitemap-index.xml / sitemap-0.xml come from @astrojs/sitemap, which walks the pages Astro actually rendered — so a new tool appears in the sitemap for the same reason it appears in the grid, with no list to maintain. A tool page carries a lastmod taken from its latest release date; a page with no such date carries none, because an invented lastmod is worse than an absent one.
  • robots.txt is the endpoint src/pages/robots.txt.js, which points at the sitemap on whatever host site names.

Hosting

The site runs on GitHub Pages today. It is also packaged as a container, so it can move to Quave ONE — where the family's analytics already run — without a rewrite. Nothing below is switched on yet; it is the path, prepared.

The image

Dockerfile is two stages:

  1. node:22-alpine runs npm ci, then the same two commands the workflow runs: npm run catalog and astro build. The container and Pages are therefore built from the same source of truth and cannot drift.
  2. nginx:1.29-alpine serves dist/ and nothing else — no Node, no node_modules, no token in the shipped image.

Two build arguments:

Arg Default Why
SITE_URL https://tui.tools The canonical host. Feeds site, and through it every canonical link, the sitemap and robots.txt.
GITHUB_TOKEN empty Lifts the GitHub API rate limit for the catalog step.

Two environment variables reach the catalog step as well, both about the package repository:

Variable Default Why
TUI_PKGS_URL https://pkgs.tui.tools Where the family's apt, dnf and pacman repository lives.
TUI_PKGS_LIVE unset true or false skips the probe below and forces the answer, for a build that has to be deterministic.

The distro channels are declared in every tool's tool.json with "available": false, because saying otherwise while nothing answers at pkgs.tui.tools would be a lie a reader discovers at their own shell prompt. So the catalog step sends one HEAD to TUI_PKGS_URL/install.sh per build. If it answers, the channels whose packages that release actually carries are promoted to available and the install pages turn the commands on. If it does not answer — not deployed, offline build, a slow day — the pages stay on coming soon, which is what they say anyway. There is no wrong answer to fall back to, and no file to edit on the day the repository goes up.

GITHUB_TOKEN is worth spelling out. Without it the catalog script uses the anonymous GitHub API, capped at 60 requests per hour per IP and shared with every other anonymous caller behind the same address. One catalog run costs roughly three requests per tool plus one for the org listing, which fits today and will not once the family grows or the build runs on a shared runner. And the script's safety net — keep the catalog already on disk — has nothing to keep in a fresh container, so a rate-limited build fails rather than shipping a stale site. Set the token for any build that is not a one-off local one. On Quave ONE that means an env var marked Build or Both, which the platform passes in as the ARG the Dockerfile declares.

nginx.conf is copied in as a template: the entrypoint runs envsubst over it at start-up, so listen ${PORT} resolves then. The image defaults to PORT=3000, which is Quave ONE's default app port — keep the two equal. NGINX_ENVSUBST_FILTER limits the substitution to PORT so nginx's own $uri survives it. The server block gives /_astro/ a year (Astro fingerprints those names), other images a day, HTML and the SEO files no-cache, serves a real 404.html instead of the SPA-style "index.html with a 200", answers /healthz for the platform probe, and sets the security headers from security-headers.conf.

Build and check it locally:

docker build \
  --build-arg SITE_URL=https://tui.tools \
  --build-arg GITHUB_TOKEN="$(gh auth token)" \
  -t tui-tools-site .

docker run --rm -p 8099:3000 tui-tools-site
curl -s localhost:8099/ | grep -o '<title>[^<]*</title>'
curl -s localhost:8099/sitemap-index.xml
curl -s localhost:8099/robots.txt

Keeping the container host current

The site's content lives outside this repository: a release in another repo changes a page here with no commit to push. That is why Pages rebuilds hourly rather than only on push, and a container host has the same problem without the same answer — nothing in its git history changed, so nothing tells it to rebuild.

publish.yml has a ship job for that. Quave ONE pulls from GitHub only where its GitHub App is installed, and it is not installed on this organization, so the job pushes instead: the documented three-step Code API — ask for a pre-signed URL, PUT the source tarball, say it landed — and the third step triggers the build. The image builds the catalog itself, so a build is a fresh site.

That leaves one wrinkle. The platform reuses the image it already built when the source is unchanged, which is right in general and wrong here: every hourly run sends identical source while the content has moved on. So the tarball carries a .build-stamp file holding the SHA-256 of the catalog that run built. Nothing changed upstream, same stamp, same source, image reused and no build minutes spent; a release shipped, the catalog changes, and the stamp changes the source and the Dockerfile's COPY . . layer with it.

The job is skipped unless the repository variable QUAVE_ENV_NAME is set:

gh variable set QUAVE_ENV_NAME  --body tui-tools-tui-site-production
gh secret   set QUAVE_API_TOKEN                  # the app-environment token

Set QUAVE_ENV_NAME without the secret and the job fails loudly rather than leaving a site that quietly stops updating. QUAVE_API_URL is optional and defaults to https://api.quave.cloud/api/public/v1.

The token is the app-environment token, not a user token: it can deploy this one environment and nothing else. Read it from the environment's page on Quave ONE, or rotate it there if it ever leaks.

If the GitHub App is ever installed

Installing the Quave ONE GitHub App on the tui-tools organization would let the platform pull main on push and drop the upload half of the ship job. It would not replace it: a push is the trigger that this site does not have — the hourly rebuild is the point — so the job would still have to run, just with forceNewBuild against the build endpoint instead of a tarball. The upload flow costs one extra minute per run and needs no click, which is why it is what is wired today.

Cutover to Quave ONE (owner action)

Steps 1–4 are done. They left Pages serving as it always did, which is the point: everything up to the DNS change is reversible, and the DNS change is the owner's.

  1. Create the apptui-site on the tui-tools account, CUSTOM preset with the root Dockerfile, port 3000, health check /healthz, SSL on, one zCloud, region us-5 (where the analytics already live). It deploys by source upload rather than from GitHub, because that needed no click; see above.

  2. Set the build variableSITE_URL=https://tui.tools, marked Build. GITHUB_TOKEN is not set and now has to be; see The build token below, which is the one thing still standing between this and a site that updates itself.

  3. Turn on the rebuild signalQUAVE_ENV_NAME and QUAVE_API_TOKEN are set, and the ship job runs on every publish.

  4. Add the hoststui.tools and www.tui.tools on the site environment, analytics.tui.tools on the Umami one.

  5. Point DNS — done in Cloudflare, and all three names are VALID with Let's Encrypt certificates issued. The records:

    Type Name Value
    CNAME, flattened tui.tools tui-tools-tui-site-production.zcloud.services
    CNAME www.tui.tools tui-tools-tui-site-production.zcloud.services
    CNAME analytics.tui.tools tui-tools-tui-analytics-production.zcloud.services

    The apex is the interesting one. tui.tools cannot hold a plain CNAME — no apex can — so it relies on Cloudflare's CNAME flattening, which resolves the target and answers with A records. An ALIAS/ANAME record does the same thing at registrars that offer it; one that offers neither cannot serve an apex from a container host at all, and the answer there is to host at www.tui.tools and redirect the apex to it.

    The records are DNS-only, not proxied. Proxying them through Cloudflare would work but needs an Origin CA certificate on the Quave ONE side; see the platform's Cloudflare as a proxy guide before turning the orange cloud on.

  6. Set SITE_DOMAIN — already tui.tools, so the Pages build renders the same canonical host the container does. For the overlap in which both are up, they agree.

  7. Keep Pages, which is a change of plan and worth the paragraph. The original step here was to delete the deploy job once Quave ONE had proven itself. Do not: tui-tools.github.io is the URL every existing link and bookmark uses, and the redirect from it to tui.tools is something GitHub does only while Pages is enabled with the custom domain set. Turn Pages off and those links 404 rather than arriving. So Pages stays, its job now being to serve one redirect, and dist/CNAME — written by the write CNAME step from the SITE_DOMAIN variable — is what tells it where to send them.

    www.tui.tools is the other half of the same idea and is handled in the image instead: nginx.conf answers that name with a 301 to the apex, preserving the path. Two hosts serving identical HTML is a duplicate a crawler has to resolve on its own; a redirect resolves it first.

Two loose ends beyond the token:

  • The Umami tag in src/layouts/Base.astro now points at https://analytics.tui.tools/script.js, which ships with the next successful build. Change the website's domain inside Umami to match, so the numbers keep accruing to the same site rather than starting a new one.
  • The analytics environment also carries a host analytic.tui.tools — singular, a typo, with no DNS behind it. It will sit invalid until the platform auto-disables it. Remove it from the Hosts tab.

The build token (owner action)

The container build needs a GitHub token, and this is no longer theoretical. The first build squeaked under the anonymous limit; the second, minutes later, died at the sixteenth of seventeen repositories:

GET https://api.github.com/repos/tui-tools/tui-samba/releases/latest → 403
API rate limit exceeded for 88.198.69.252.

That address is the platform's build node, shared with every other tenant building on it, and the site's own catalog run costs about fifty requests against a budget of sixty an hour. An hourly rebuild cannot fit in that, and a rate-limited build fails rather than shipping something stale — the catalog script's fallback is "keep what is on disk", and a fresh container has nothing. So until the token exists, the environment keeps serving the last image that built and stops picking up releases.

Create a fine-grained personal access token:

  • Resource owner: the tui-tools organization (or your own account — everything it reads is public).
  • Repository access: Public repositories (read-only).
  • Permissions: none beyond the default Metadata: Read. It reads public repository contents and releases, which needs no grant.
  • Expiry: as long as you are willing to rotate.

Then set it on the environment as GITHUB_TOKEN, Used for: Build. The next publish run picks it up. Nothing in this repository needs the token: it is a Quave ONE environment variable, and the Actions build has the runner's own token with a far higher limit.

Local development

npm install
GITHUB_TOKEN=$(gh auth token) npm run dev     # catalog, then the dev server
GITHUB_TOKEN=$(gh auth token) npm run build   # catalog, then a static build
npm run build:only                            # rebuild the pages, reuse the catalog
npm run preview

GITHUB_TOKEN is optional — without it the catalog script uses the anonymous API and its 60-requests-per-hour limit, which is enough for one run. If a run fails while a catalog.json is already on disk, the script keeps the old one and exits successfully, so a rate limit or a dropped connection cannot leave you without a site to build.

src/data/catalog.json, public/tools/ and public/og/ are generated and gitignored. npm run og redraws the previews on their own, against the catalog already on disk.

What is in here

Path What it is
scripts/build-catalog.mjs The whole data layer: org → manifests → releases → images → catalog.json
scripts/build-og.mjs The 1200×630 link preview each page points og:image at
src/pages/index.astro The marketplace grid, with a category filter
src/pages/tools/[name].astro A tool: gallery, description, keys, compatibility, install picker, security, downloads, releases
src/pages/install.astro Family-level install: the repository setup per package manager, and how to verify a download
src/pages/security.astro The family's principles, every tool's answers, and how to report
src/pages/kit.astro What tui-kit is
src/pages/robots.txt.js robots.txt, built from site so no host is hardcoded
src/components/CommandDialog.astro The site's one borrowed idea, below
src/lib/markdown.js The safe markdown subset a manifest's description may use
src/styles/global.css Tokyo Night, and the type rule: the machine speaks in mono, we speak in sans

The install page

Analytics

The site counts visits with Umami, self-hosted on Quave ONE. Nothing is sent to Google, and there is no third-party network the numbers feed into.

What is collected. A page view: the path, the referrer, the screen size, the browser and OS names, and a country derived from the request. Plus the named events below, each with a couple of labels. That is the whole list.

What is not. No cookies and no localStorage: Umami stores nothing in your browser. No IP address is kept — it is hashed together with a daily-rotating salt to recognise a repeat visit within one day, and cannot be reversed or linked across days. No cross-site identifier, no profile, no ad network.

Events. These names are stable; treat them as an interface, and add rather than rename.

Event Fired when Props
install-copy A command's copy button is pressed tool, manager
distro-select A package manager tab is chosen on a tool page manager
download A release asset link is clicked tool, asset
repo-click A tool's source repository link is clicked tool

tool is a tool name (or family for a command that is not tool-specific), manager is the install path (pacman, aur, apt, dnf, zypper, binary, source, …).

How to opt out. Turn on "Do Not Track" in your browser and the script stands down: the tag carries data-do-not-track="true", so it sends nothing at all. Any content blocker also stops it, and the site works exactly the same without it — nothing on the page waits for the script, and nothing breaks when it never arrives.

The design, in one paragraph

Every tool in the family shows a command inside a box and asks y or n before running it. Installing a tool is previewed the same way here: every command on the site sits in that same dialog, and the copy button is the y key. The palette is Tokyo Night, carrying the meaning the branding gives it — green marks what the family shares, blue marks what one tool adds, so family pages accent green and a tool's page accents blue. Anything the machine says is set in monospace; anything we say is set in sans. Fonts are a system stack and every image is served from this repository; the one request that leaves the page is the analytics script described above.

Unofficial

These tools follow the Omarchy visual style and read its theme files. They are not part of the Omarchy project and are not endorsed by its maintainers.

MIT — see LICENSE.

About

The tui-tools family website: a marketplace of terminal tools for Linux, built from each tool's tool.json manifest.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages