From c3ff8f85cef6068ad899af4f6832b0b4eb1a7a24 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 16 Aug 2026 07:44:03 +0000 Subject: [PATCH] docs: give the README an install and usage list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README explained the design and skipped the part someone actually arrives for. Now it opens with a command table, then requirements, then install, then usage per command with runnable examples — the same shape the scripts repo README had, which was the one people used. Renames the link scripts to link:bin / unlink:bin. `link` and `unlink` are pnpm builtins, so `pnpm link` runs pnpm's own command and never touches this script — the README documented a step that would silently do nothing. Every command in the install section was run as written before committing. Also records requirements the tools have always had and never stated: gh authenticated, dig at /usr/bin/dig for domainjson, and OpenRDAP on PATH for its RDAP half. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 262 ++++++++++++++++++++++++++++++++++++--------------- package.json | 5 +- 2 files changed, 187 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 22d5962..844718c 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,110 @@ # cli-tools -Local command-line tools, in TypeScript, on PATH. - -Ported from the bash and JavaScript originals in -[`profullstack/scripts`](https://github.com/profullstack/scripts). The point of -the port is not the language — it is the two things bash was making expensive: - -- **Typed, validated responses.** Every `gh` call used to go through `jq -r` into - a string compare. `jq -r '.mergeable'` on a response that never had the field - prints the four characters `null`, which is not `MERGEABLE`, so a perfectly - mergeable PR read as ineligible *for a reason nobody wrote*. Now an - unrecognised field is named in an error instead of silently becoming a string. -- **Tests.** The originals had none. Verifying a change meant running it against - live pull requests, which is a poor place to discover you were wrong. - -## Commands +Command-line tools for working across a lot of repositories at once, in +TypeScript, installed as executables on `PATH`. | Command | What it does | | --- | --- | -| `gh-prs` | List every open PR across the owners you name | -| `gh-prs-merge` | Sweep open PRs and squash-merge the ones genuinely ready | -| `gh-prs-fix-all` | Fix the open threatcrush-scan PRs that are broken because of us | -| `tcfeed` | Find repositories worth scanning, scan them, print a shortlist | -| `domainjson` | whois-style, JSON-first name lookup | +| [`gh-prs`](#gh-prs) | List every open PR across the owners you name | +| [`gh-prs-merge`](#gh-prs-merge) | Squash-merge the PRs that are genuinely ready | +| [`gh-prs-fix-all`](#gh-prs-fix-all) | Fix the open threatcrush-scan PRs that are broken because of us | +| [`tcfeed`](#tcfeed) | Find repositories worth scanning, scan them, print a shortlist | +| [`domainjson`](#domainjson) | whois-style, JSON-first name lookup | + +## Requirements + +- **Node 20+** +- **[`gh`](https://cli.github.com/)**, authenticated (`gh auth status`) — every + `gh-prs*` command shells out to it +- **`dig`** at `/usr/bin/dig` — `domainjson` only +- **[OpenRDAP](https://github.com/openrdap/rdap)** (`rdap` on `PATH`, or + `~/go/bin/rdap`) — `domainjson` only, and it degrades to DNS-only without it ## Install -```bash +```sh +git clone git@github.com:profullstack/cli-tools.git ~/src/profullstack/cli-tools +cd ~/src/profullstack/cli-tools pnpm install -pnpm link # symlink bin/*.ts into ~/.local/bin +pnpm link:bin ``` -The names already exist in `~/.local/bin` pointing at `~/scripts/bin`, so a -plain run reports them as not-ours and changes nothing. To migrate: +`link:bin` symlinks every `bin/*.ts` into `~/.local/bin` without the extension, +so `gh-prs-merge` is a real command. (Not named `link` — that is a pnpm builtin, +and `pnpm link` would run pnpm's own command instead of this one.) Make sure the +directory is on `PATH`: -```bash -node scripts/install-links.mjs --dry-run --force # see what would move -node scripts/install-links.mjs --force # take them over +```sh +export PATH="$HOME/.local/bin:$PATH" ``` -`--force` takes over a *symlink*. A real file of the same name is still -refused — clobbering someone's actual binary to install a convenience is not a -trade a script gets to make on its own. +### Migrating from `profullstack/scripts` + +These names already exist in `~/.local/bin` pointing at `~/scripts/bin`, so a +plain `pnpm link:bin` reports them as not-ours and changes nothing. To take them +over: + +```sh +node scripts/install-links.mjs --dry-run --force # see exactly what would move +node scripts/install-links.mjs --force # do it +``` + +`--force` replaces a *symlink*. A real file of the same name is still refused — +clobbering someone's actual binary to install a convenience is not a trade a +script gets to make on its own. To go back: -```bash -pnpm unlink # remove the ones we own +```sh +pnpm unlink:bin # remove ours ln -sf ~/scripts/bin/gh-prs-merge ~/.local/bin/gh-prs-merge # and so on ``` -## Files on PATH, not shell functions +## Usage -These install as executables on PATH rather than shell aliases or functions. +### `gh-prs` -The older tools carry a comment saying this is because the moshcode pit runs -aliases with `zsh -c`, a non-interactive shell that reads neither `~/.zshrc` nor -`~/.zsh_aliases`. **That is no longer true** — `src/aliases.mjs` in current -moshcode runs `$SHELL -ic`, which is interactive and does source them. Verified: +Lists open pull requests across any number of organizations and personal +accounts, newest first, as an aligned table. In a capable terminal the PR number +and URL become clickable. -```console -$ zsh -ic 'gh-prs-all --help' # works — the pit's path -$ zsh -c 'gh-prs-all --help' # zsh:1: command not found +```sh +gh-prs --orgs profullstack,moshcoder,h4kr,infernetprotocol +gh-prs --users ralyodio +gh-prs --orgs profullstack --users ralyodio --limit 50 +gh-prs --orgs profullstack --no-links # plain text, for piping ``` -The reason to stay on PATH is the weaker but still sufficient one: a file works -from every caller — an interactive shell, `zsh -c`, a systemd unit, a CI step — -without anything having been sourced first. A shell alias only works where a -startup file was read. +### `gh-prs-merge` -Nothing should alias *to* these either. A function beats PATH, so a wrapper of -the same name silently shadows the file and the two drift apart. - -Pit aliases (`/alias set ""`, stored in -`~/.moshcode/aliases.json`): +Walks the same scopes and squash-merges every PR that qualifies, oldest first. +**Dry run by default** — nothing changes until you pass `--apply`. +```sh +gh-prs-merge --orgs profullstack # report only +gh-prs-merge --orgs profullstack --apply # merge what qualifies +gh-prs-merge --orgs profullstack --apply --fix # repair, then merge +gh-prs-merge --orgs profullstack --apply --fix --fix-wait 900 ``` -/alias set prs "gh-prs --orgs profullstack" -/alias set merge "gh-prs-merge --orgs profullstack --apply --fix" -/alias set merge-dry "gh-prs-merge --orgs profullstack" -/alias set fixprs "gh-prs-fix-all" -/alias set feed "tcfeed" -/alias set whoisj "domainjson" -``` -## `gh-prs-merge --fix` +A PR is merged only when all of these hold: + +- it is open, and not a draft (or was successfully marked ready) +- `mergeable` is `MERGEABLE` and `mergeStateStatus` is `CLEAN` +- at least one CI check exists, unless `--allow-no-checks` +- every check is `pass` or `skipping` +- the head commit has not changed when the merge is submitted + +That last one is the safety property. Between reading the checks and submitting +the merge, someone can push; `--match-head-commit` means the merge lands on the +commit that was actually verified or not at all. There is deliberately no +`--admin`, so branch protections stay enforced. + +#### `--fix` -A skip is not always a verdict on the PR. Two PRs were once skipped as -`mergeStateStatus=UNSTABLE` purely because a check had not reported yet; nothing -was wrong with either, and both merged unchanged minutes later. +A skip is not always a verdict on the PR. Two were once skipped as +`mergeStateStatus=UNSTABLE` purely because a check had not reported yet — +nothing was wrong with either, and both merged unchanged minutes later. `--fix` repairs a repairable skip **once**, then judges the PR again against the identical rules. It requires `--apply`, because every repair writes. @@ -107,26 +123,94 @@ What it will not do is as much of the design: reviewed. - **A check that ran and failed is a result, not an obstacle.** Retrying until it passes is how a flaky suite becomes a green one that means nothing. -- **No `--admin`.** Branch protections stay enforced. -## Nothing under `bin/` does work at import time +### `gh-prs-fix-all` -Every entry point guards its side effects with `isMain(import.meta.url)`, and -anything worth testing lives in `src/`. +Looks at every open threatcrush-scan pull request and fixes the ones broken +because of us. Reports the rest and leaves them alone. -This is not decorative. A test that imported `bin/gh-prs-fix-all.ts` to reach one -pure function *ran the tool*: the suite went from 60ms to 93 seconds and swept -live pull requests with `--fix` implied. The guard and the `src/` split are both -that lesson. +```sh +gh-prs-fix-all # fix ours, report theirs +gh-prs-fix-all --dry-run # change nothing, just say what stands +gh-prs-fix-all owner/name ... # only these +``` + +The name says fix-all and it will not fix all, deliberately. Pushing to a fork +sets off whatever the upstream repo runs on push, so their suite goes red +against a commit that only added files under `.github/`. Those are reported, +never touched. + +### `tcfeed` + +```sh +tcfeed # the 50 newest posts +tcfeed 100 # more of them +tcfeed --forget # look at everything again next time +tcfeed pr owner/name [--dry-run] # install the scan workflow +tcfeed check [--fix] # how are the open requests doing +``` + +The scanner itself lives in the threatcrush checkout, so this is a launcher. +Point it elsewhere with `TCFEED_REPO`; every other `TCFEED_*` variable is read +by the script it launches and works unchanged. -The `realpath` in `isMain` matters too — these install as symlinks, so -`process.argv[1]` is the link while `import.meta.url` is its target. Comparing -them raw reports "imported" for every installed command, disabling all of them at -once. +### `domainjson` + +One JSON object on stdout: `{ name, rdap | moshpit, dns }`. + +```sh +domainjson example.com +domainjson --name example.com +domainjson --registry https://pit.moshcode.sh --timeout 4000 example.hacker +domainjson -s https://rdap.example example.com # OpenRDAP flags pass through +``` + +Names ending in a Moshpit TLD are served from the registry API; everything else +goes through OpenRDAP. Either way `dig` adds records, hosts, reverse lookups and +per-nameserver AXFR attempts. Errors are JSON too — a tool whose output gets +parsed should not change shape when it fails. + +## Aliases + +Pit aliases live in `~/.moshcode/aliases.json`: + +``` +/alias set prs "gh-prs --orgs profullstack" +/alias set merge "gh-prs-merge --orgs profullstack --apply --fix" +/alias set merge-dry "gh-prs-merge --orgs profullstack" +/alias set fixprs "gh-prs-fix-all" +/alias set feed "tcfeed" +/alias set whoisj "domainjson" + +/alias # list +/alias get merge # show one +/alias rm merge # forget one +``` + +Arguments append rather than substitute, so `/merge --limit 5` works. + +### Why these are files on `PATH` + +The older tools carry a comment saying it is because the moshcode pit runs +aliases with `zsh -c`, a non-interactive shell that reads neither `~/.zshrc` nor +`~/.zsh_aliases`. **That is no longer true** — current moshcode runs +`$SHELL -ic`, which is interactive and does source them: + +```console +$ zsh -ic 'gh-prs-all --help' # works — the pit's actual path +$ zsh -c 'gh-prs-all --help' # zsh:1: command not found +``` + +The reason to stay on `PATH` is the weaker but sufficient one: a file works from +every caller — an interactive shell, `zsh -c`, a systemd unit, a CI step — +without anything having been sourced first. + +Nothing should alias *to* these either. A function beats `PATH`, so a wrapper of +the same name silently shadows the file and the two drift apart. ## Development -```bash +```sh pnpm test # vitest pnpm typecheck # tsc --noEmit ``` @@ -135,11 +219,33 @@ Tests stub the subprocess layer rather than the network, so `gh` is never invoked. The suite runs in well under a second; if it starts taking longer, something is reaching the network that should not be. -## Differences from the originals +**Nothing under `bin/` does work at import time.** Every entry point guards its +side effects with `isMain(import.meta.url)`, and anything worth testing lives in +`src/`. That is not decorative: a test that imported `bin/gh-prs-fix-all.ts` to +reach one pure function *ran the tool*, taking the suite from 60ms to 93 seconds +and sweeping live pull requests with `--fix` implied. + +`isMain` resolves the realpath first, because these install as symlinks — +`process.argv[1]` is the link while `import.meta.url` is its target, and +comparing them raw reports "imported" for every installed command at once. + +## Why TypeScript -Deliberate, and small: +Ported from the bash and JavaScript originals in +[`profullstack/scripts`](https://github.com/profullstack/scripts). The point was +not the language. It was the two things bash was making expensive: + +- **Typed, validated responses.** Every `gh` call went through `jq -r` into a + string compare. `jq -r '.mergeable'` on a response that never had the field + prints the four characters `null`, which is not `MERGEABLE` — so a perfectly + mergeable PR read as ineligible *for a reason nobody wrote*, indistinguishable + from a real verdict. An unrecognised field is now named in an error. +- **Tests.** The originals had none, so verifying a change meant running it + against live pull requests. + +## Differences from the originals - `gh-prs` prints `No open PRs found.` instead of a bare header row. - `gh-prs-merge` adds `fixed=` to its summary line. -- `domainjson` output is unchanged in structure; DNS answers arrive in - round-robin order, so array ordering varies between runs of either version. +- `domainjson` is unchanged in structure. DNS answers arrive round-robin, so + array ordering varies between runs of either version. diff --git a/package.json b/package.json index 0e0ebd5..b567491 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,9 @@ "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc -p tsconfig.json --noEmit", - "link": "node scripts/install-links.mjs", - "unlink": "node scripts/install-links.mjs --remove" + "//link": "Not named `link`/`unlink`: those are pnpm builtins, and a bare `pnpm link` runs pnpm's own command rather than this script.", + "link:bin": "node scripts/install-links.mjs", + "unlink:bin": "node scripts/install-links.mjs --remove" }, "devDependencies": { "@types/node": "^22.10.2",