Skip to content

Releases: profullstack/cli-tools

v0.17.0 — hqtui

Choose a tag to compare

@ralyodio ralyodio released this 30 Aug 07:28

hqtui — every vital of a box, in the terminal

The HQTUI dashboard, wrapped so it is a command:

hqtui                     # this machine, live
hqtui --sim               # the deterministic simulation
hqtui --screen traffic    # open on a screen
hqtui --help              # it is upstream's CLI: upstream's flags

Ten screens. Beyond CPU, memory, disks and processes it reads what a system
monitor usually leaves alone: live sockets grouped by protocol, TCP/UDP/ICMP
counters with a retransmit rate, HTTP requests parsed out of nginx/apache
access logs, sshd authentication events, who is logged in, wtmp login history,
systemd units, containers, kernel counters and filesystem inode usage.

All of that unprivileged. sudo additionally exposes socket process names,
failed logins from btmp, access logs and the full journal — and sudo drops
your PATH:

sudo -E env "PATH=$PATH" hqtui

It does not add temperatures. On a VM there are none to add: a KVM or Xen
guest is never shown the host's thermal hardware, and the dashboard says which
case it is rather than leaving the panel blank.

Two flags are ours, spelled --self-* because every plain word belongs to the
dashboard:

hqtui --self-update       # reinstall the latest release
hqtui --self-where        # which copy runs, and from where

The first run installs it into ~/.local/share/cli-tools/vendor/hqtui rather
than globally. Both @profullstack/hqtui-demo (hqtui-demo) and the library
@profullstack/hqtui (hqtui, the same name as this wrapper) ship an
executable that would otherwise collide on PATH. Installed rather than
npx-ed because the box you are SSHing into because something is wrong is the
worst moment to need the registry before you can look at it.

Also in this release

root-ubuntu.sh installs the full hardware sensor and inventory set by
default — lm-sensors, i2c-tools, smartmontools, nvme-cli, ipmitool, acpi,
powertop, sysstat, dmidecode, lshw, pciutils, usbutils — and runs
sensors-detect on bare metal only, since on a guest it probes a machine with
no sensor chips and leaves an empty config that reads like a failure.

Upgrading

cli-tools update
# or
curl -fsSL https://raw.githubusercontent.com/profullstack/cli-tools/master/install.sh | sh

v0.16.0 — sysupdate

Choose a tag to compare

@ralyodio ralyodio released this 30 Aug 07:00
ee29b26

One new command, and a provisioner that stops leaving boxes with no slack.

sysupdate

Updating a machine was three commands nobody could quite remember the order of. Now it is one, and the pit gets it as /update.

sysupdate                # apt update, apt upgrade, snap refresh
sysupdate --yes          # ...without stopping to ask
sysupdate --no-snap      # apt only
sysupdate --dry-run      # print the commands, run nothing

It runs the three steps in order and stops at the first that fails, which is the && the one-liner had: there is no point upgrading against package lists that failed to refresh.

Three decisions worth knowing about:

  • apt, not apt-get. They are not the same command — apt upgrade installs a package that needs a new dependency, apt-get upgrade holds it back. That difference is how kernels and security updates quietly never land on a box everybody believes is current.
  • sudo only when not already root. A minimal image may have no sudo on it at all, and asking for it there fails for a reason that has nothing to do with updating anything.
  • A box with no snapd skips that step and says so, rather than reporting a failure for something that was never going to run. A box with no apt is refused outright rather than left two thirds updated.

When the upgrade lands a kernel or a libc it names the packages and says a reboot is required — that being exactly the moment people stop thinking about it.

It is called sysupdate and not update because cli-tools update already means "move this checkout to the current commit". One word cannot usefully mean both that and "upgrade the operating system". The pit alias is /update, which is safe because nothing on PATH answers to that name.

root-ubuntu.sh: 2G of swap on a box that has none

A box with no swap has no slack: the kernel's only answer to a memory spike is the OOM killer, and what it picks is whatever was biggest — the build, the language server, the editor someone was working in.

The step only acts where there is no swap whatsoever, so a machine with a swap partition or zram is left exactly as it is rather than gaining a second swapfile nobody remembers making. It declines where a plain swapfile is wrong or dangerous (btrfs needs chattr +C and no compression; a swapfile on zfs can deadlock the box under exactly the pressure it was added to survive), inside a container where the kernel and its swap belong to the host, and when the disk cannot spare the space. vm.swappiness goes to 10 alongside — the default of 60 pages out memory that is still in use, which is how swap earned its reputation. SWAP_SIZE=0 turns it off.

Also

  • one ssh-agent per user, under systemd
  • the hardware inventory set installed by default: lm-sensors, smartmontools, nvme-cli, ipmitool, acpi, powertop, with sensor detection on bare metal
  • companions: diskpush, and installers that are not npm
  • porkbun: Porkbun's own pre-flight, structured refusals kept structured, and the fact that registration spends prepaid credit rather than a card

Full changelog: v0.15.0...v0.16.0

v0.14.0 — torrent

Choose a tag to compare

@ralyodio ralyodio released this 29 Aug 16:58
1d375e8

torrent — make a torrent out of a directory, and get it seeded

torrent create ./album              # writes album.torrent, prints the magnet
torrent seed ./album                # …and hands the magnet to torlnk
torrent magnet album.torrent        # for one you already have
torrent info album.torrent

create-torrent writes a .torrent and never prints a hash; torlnk takes a magnet rather than a file. The two do not meet without something in between, and this is it.

The info hash is computed here, not by adding a dependency — a SHA-1 over the bencoded info dictionary, read verbatim. Decode and re-encode and the hash changes the moment a client wrote a key in an order we did not reproduce, which yields a well-formed magnet for a torrent that does not exist. A test pins our hash against the one a real client computed for a torrent it made itself.

Trackers were checked, not copied. A browser can only ever be a WebRTC peer, so a torrent with no wss:// tracker is invisible to every web player — on the DHT, found by desktop clients, and showing the browser a torrent with no peers, which reads as dead rather than as a missing tracker. Of the announce list the WebTorrent tooling ships by default:

tracker state
tracker.leechers-paradise.org no DNS at all
tracker.coppersurfer.tk UDP connect times out
tracker.empire-js.us UDP connect times out
tracker.btorrent.xyz self-signed cert; a browser refuses it

What ships is two WSS trackers that complete a WebSocket handshake and four UDP trackers that return a connection id.

Other notes

  • --webseed embeds HTTP URLs already serving the same bytes (BEP 19), so the torrent is downloadable before any peer has it. The URL must serve the exact bytes, or it fails its hash check in a way that looks like corruption.
  • --private is opt-in and named: a private torrent is excluded from the DHT by every client honouring the flag — the opposite of the reason to make one here.
  • Seed duration is deliberately not a flag; torlnk's --seed-time is a daemon setting, not per-torrent.

Requirements

npm i -g create-torrent for create. torrent seed also needs torlnk running — and note torlnk installs with pnpm only (pnpm add -g torlnk); npm cannot install it at all.

Upgrading

cli-tools update — the install is symlinks into a working tree, so this pulls and relinks.

v0.13.0 — dl

Choose a tag to compare

@ralyodio ralyodio released this 29 Aug 16:31
01dab4d

dl — download a video, or just its audio, through yt-dlp

A thin front for yt-dlp, in the same shape vid is a thin front for ffmpeg.

dl https://example.com/watch?v=abc          # the video
dl --height 720 https://example.com/v/abc   # capped
dl audio https://example.com/watch?v=abc    # → .m4a
dl info https://example.com/watch?v=abc     # nothing downloaded
dl formats https://example.com/watch?v=abc

Two defaults are decisions rather than plumbing:

  • --no-playlist. A YouTube link copied while a mix is playing carries list=, and yt-dlp reads that as "download all of it" — the difference between one file and two hundred, on a command whose entire input is a pasted URL.
  • ffmpeg-awareness in the format selector. Above ~720p picture and sound arrive as separate streams needing a mux, so without ffmpeg the bv*+ba alternatives are a wasted download: yt-dlp fetches both halves and only then finds it cannot merge. Without ffmpeg dl restricts itself to single-stream formats and says so. dl audio is -x, which is ffmpeg, so there it is a hard requirement.

findBinary grew a probe flag: it ran candidates with -version, which ImageMagick and ffmpeg answer 0 — but yt-dlp's parser reads that single dash as seven combined short options and exits non-zero, so probing it the same way reported an installed binary as missing.

Installing what it needs

moshcode v0.66+ can install the binaries: moshcode install yt-dlp, moshcode install ffmpeg, moshcode install imagemagick.

Upgrading

cli-tools update — the install is symlinks into a working tree, so this pulls and relinks rather than re-cloning.

Note: v0.12.0 was never tagged; its work (companions) is in this history.

v0.11.0 — DEFAULT_GROUPS

Choose a tag to compare

@ralyodio ralyodio released this 29 Aug 07:06

Fixed

DEFAULT_GROUPS is documented in server.conf.example, but root-ubuntu.sh declared it as an unconditional assignment (DEFAULT_GROUPS="sudo,admin") instead of the "${KEY:-default}" form every other setting uses. read_server_config runs ~200 lines earlier, so the configured value — and any value passed in the environment, which the file header promises always wins — was read correctly and then silently discarded.

This was not cosmetic. The group prompt is skipped when there is no tty, so an unattended run (--refresh, or the documented curl … | bash) takes $DEFAULT_GROUPS verbatim for every account it creates. A box that had configured www-data,users,docker still put new accounts in sudo,admin — root-equivalent, and the opposite of what was asked for — with nothing in the run output looking wrong.

DEFAULT_GROUPS was the only affected key; the other fifteen server.conf.example documents were already overridable.

Added

Tests covering the class rather than the single key: every documented key must survive its own declaration, plus a behavioural check that reads a config file and then runs the real declaration line out of the script. Both fail against the old line.

DEFAULT_GROUPS is now documented in the script header and the README, since an unattended run never prompts and this is what silently decides how privileged new accounts are.

Upgrading: re-fetch the script — curl -fsSLO https://raw.githubusercontent.com/profullstack/cli-tools/master/root-ubuntu.sh. If you set DEFAULT_GROUPS before this release, check where existing accounts actually landed: ./root-ubuntu.sh groups.

v0.10.0 — shorten

Choose a tag to compare

@ralyodio ralyodio released this 28 Aug 20:33

One new command, and the first tagged release since v0.3.0.

shorten

shorten https://pit.moshcode.sh/n/blue.eggs/the-post-i-wrote-on-tuesday
#      → https://pit.moshcode.sh/f/k7mq2xd

Mints a short link on the Moshpit registry and prints it. /f/<code> answers a 302 to wherever it points, from anywhere — no resolver, no extension, nothing installed on the other end. shorten list shows yours with hit counts, shorten rm <code> takes one down, and --name blue.eggs files a link under a Moshpit name you hold.

Nothing to configure on a machine where the pit works. The token moshcode login already wrote is read from ~/.moshcode/credentials.json. A new moshcode key in the credential store (cli-tools config set moshcode) and MOSHCODE_API_KEY cover a box that has the key but not moshcode.

It is a client, not a second shortener. The registry owns the codes and the rules about what may be redirected to — http(s) only, and a short link may not point at itself — and its refusals are passed through verbatim rather than reimplemented here, where the two copies would drift apart the first time one of them was tightened. Minting is authenticated for the same reason it is on the other side: an anonymous shortener is an open redirector with a database attached. It is idempotent per account, so the same url twice returns the same code and a retry cannot split one destination's hits in half.

The short url is the only thing on stdout, so shorten … --bare | pbcopy gets a url and nothing else.

The pit has the same thing as /shorten, shipped in moshcode v0.69.0.

What else has landed since v0.3.0

Tagging stopped for a while — the install is a curl … | sh one-liner that pulls master, so a release was never the distribution channel here and nothing waited on one. Six versions went by in the meantime:

  • porkbun — read and change DNS at Porkbun, and un-park a domain (0.9.0)
  • favicon — every icon a site links, rendered from one SVG (0.8.0)
  • root-ubuntu.sh — provision an Ubuntu/Debian server from one file
  • codeburn — where your AI spend goes, by task, tool, model and project (0.6.0)
  • img and vid — sharp/ImageMagick and ffmpeg, wrapped (0.5.0)
  • genrewatch — what is coming out, and whether it exists at all (0.4.0)
  • ask-web and tts — Perplexity and ElevenLabs
  • affiliate — a cursor over a list of signup pages
  • cli-tools config — a 0600 credential store, and config pull from the team vault
  • cli-tools autoupdate — a user systemd timer that keeps the checkout current

Installing

curl -fsSL https://raw.githubusercontent.com/profullstack/cli-tools/master/install.sh | sh

Unchanged: the installer adopts an existing checkout rather than cloning a second copy, and cli-tools update pulls and relinks. This tag is a marker in the history, not a separate artifact to download.

Suite is 420 tests.

v0.3.0 — generate-names

Choose a tag to compare

@ralyodio ralyodio released this 19 Aug 14:03
171cd2f

Completes the naming pipeline: describe a product in a sentence, get a thousand candidates, keep only the ones you can register.

generate-names "a registry that checks whether Lean proofs actually compile" | domainfree

New in 0.3.0

generate-names

Turns a sentence into candidate names, ready to pipe into domainfree.

generate-names "a tool that finds dead states in agent graphs" -n 1000 --tld dev
generate-names "an open directory of independent blogs" --words 1

It asks the model for vocabulary, not for a thousand names. One cheap call returns ~40 head words and ~40 modifiers; the cross product is expanded locally and shuffled. Asking a model for 1,000 names directly repeats itself within a few hundred, drifts off the brief, and costs far more for a worse list — this way the call count is identical whether you want 10 names or 10,000.

Uses whichever of OPENAI_API_KEY / ANTHROPIC_API_KEY is set, OpenAI first when both are. Cheap tier by default on each side (gpt-4.1-mini / claude-haiku-4-5), overridable with --model. Called over plain fetch — this repo still has zero runtime dependencies.

Defaults to two English words and .com; --tld, --words, --count and --seed change that.

/domain:names

The generator as a moshcode command, alongside /domain:free and /domain:lookup.

moshcode plugin marketplace add profullstack/cli-tools
moshcode plugin install domain@cli-tools

Fixed

domainfree --help used generate-names as a placeholder for "whatever produces your list", which read as a command that existed and got command not found. It exists now, and the help says so honestly.

Verified against the live API

1,000 names generated (all unique, all .com); the documented pipeline end to end returned 9 registerable domains from 40 candidates; suite 137/137; typecheck clean.

Installation note

~/.local/bin/* may still point at ~/scripts/bin for the older commands — install-links.mjs never clobbers a link it does not own. Use --force to switch them over.

v0.2.0 — domainfree

Choose a tag to compare

@ralyodio ralyodio released this 19 Aug 11:48
ebb1436

First tagged release of @profullstack/cli-tools.

The package is private: true and not on npm, so this is a tag and a set of notes rather than a publish. Install by cloning and running pnpm install && pnpm link:bin.

New in 0.2.0

domainfree — find the domains you can actually register

domainfree sorrycheck.com sinkstate.com
domainfree --file candidates.txt
generate-names | domainfree --jobs 24

Only registerable names go to stdout, one per line, so it pipes into anything. Roughly 8,500 names in 45 seconds at the default concurrency.

Availability is read from RDAP, never inferred from DNS, because DNS cannot tell registration apart from configuration: a parked domain resolves and is taken, and a domain registered with no nameservers returns NXDOMAIN exactly like a name nobody owns.

Over 8,513 generated candidates, the DNS shortcut (dig NAME | grep "ANSWER: 0") reported 20 registered domains as free and missed none that were genuinely free. oubliette.com is the case to remember: registered in 1996, paid through 2034, three nameservers, no A record.

An indeterminate response — 429, 5xx, timeout — is retried once and then reported as ERR:<code>, never as available, and the exit status is 2. A name wrongly reported free is the only failure here that costs real time.

domain plugin

Registered in the marketplace alongside blog:

moshcode plugin marketplace add profullstack/cli-tools
moshcode plugin install domain@cli-tools
  • /domain:free — filter a list of candidates down to registerable names
  • /domain:lookup — everything about one name (RDAP record, dates, nameservers, DNS, reverse PTR) as JSON, wrapping the existing domainjson, which previously had no plugin

Also in this repo as of 0.2.0

Command Does
blog-post Publish to the plain-HTML blog: next post number, valid template, index entry, feed rebuild, and a lint that catches what silently breaks RSS
domainjson Whois-style, JSON-first lookup of a single name
domainfree Bulk availability across thousands of names
gh-prs Open pull requests across orgs and users, as an aligned table
gh-prs-merge Squash-merge every PR that qualifies; dry run by default
gh-prs-fix-all Repair PRs that need it
tcfeed Read a subreddit, scan the repos its posts link, shortlist what is worth reading

Installation note

install-links.mjs will report every command as "points elsewhere" if ~/.local/bin/* still points at ~/scripts/bin, which is the current state on the dev box. That is deliberate — the script never clobbers a link it does not own. Use --force to switch them over to this repo.