Skip to content

Commit fcaaebb

Browse files
ralyodioclaude
andauthored
docs: give the README an install and usage list (#4)
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) <noreply@anthropic.com>
1 parent d0f666a commit fcaaebb

2 files changed

Lines changed: 187 additions & 80 deletions

File tree

README.md

Lines changed: 184 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,110 @@
11
# cli-tools
22

3-
Local command-line tools, in TypeScript, on PATH.
4-
5-
Ported from the bash and JavaScript originals in
6-
[`profullstack/scripts`](https://github.com/profullstack/scripts). The point of
7-
the port is not the language — it is the two things bash was making expensive:
8-
9-
- **Typed, validated responses.** Every `gh` call used to go through `jq -r` into
10-
a string compare. `jq -r '.mergeable'` on a response that never had the field
11-
prints the four characters `null`, which is not `MERGEABLE`, so a perfectly
12-
mergeable PR read as ineligible *for a reason nobody wrote*. Now an
13-
unrecognised field is named in an error instead of silently becoming a string.
14-
- **Tests.** The originals had none. Verifying a change meant running it against
15-
live pull requests, which is a poor place to discover you were wrong.
16-
17-
## Commands
3+
Command-line tools for working across a lot of repositories at once, in
4+
TypeScript, installed as executables on `PATH`.
185

196
| Command | What it does |
207
| --- | --- |
21-
| `gh-prs` | List every open PR across the owners you name |
22-
| `gh-prs-merge` | Sweep open PRs and squash-merge the ones genuinely ready |
23-
| `gh-prs-fix-all` | Fix the open threatcrush-scan PRs that are broken because of us |
24-
| `tcfeed` | Find repositories worth scanning, scan them, print a shortlist |
25-
| `domainjson` | whois-style, JSON-first name lookup |
8+
| [`gh-prs`](#gh-prs) | List every open PR across the owners you name |
9+
| [`gh-prs-merge`](#gh-prs-merge) | Squash-merge the PRs that are genuinely ready |
10+
| [`gh-prs-fix-all`](#gh-prs-fix-all) | Fix the open threatcrush-scan PRs that are broken because of us |
11+
| [`tcfeed`](#tcfeed) | Find repositories worth scanning, scan them, print a shortlist |
12+
| [`domainjson`](#domainjson) | whois-style, JSON-first name lookup |
13+
14+
## Requirements
15+
16+
- **Node 20+**
17+
- **[`gh`](https://cli.github.com/)**, authenticated (`gh auth status`) — every
18+
`gh-prs*` command shells out to it
19+
- **`dig`** at `/usr/bin/dig``domainjson` only
20+
- **[OpenRDAP](https://github.com/openrdap/rdap)** (`rdap` on `PATH`, or
21+
`~/go/bin/rdap`) — `domainjson` only, and it degrades to DNS-only without it
2622

2723
## Install
2824

29-
```bash
25+
```sh
26+
git clone git@github.com:profullstack/cli-tools.git ~/src/profullstack/cli-tools
27+
cd ~/src/profullstack/cli-tools
3028
pnpm install
31-
pnpm link # symlink bin/*.ts into ~/.local/bin
29+
pnpm link:bin
3230
```
3331

34-
The names already exist in `~/.local/bin` pointing at `~/scripts/bin`, so a
35-
plain run reports them as not-ours and changes nothing. To migrate:
32+
`link:bin` symlinks every `bin/*.ts` into `~/.local/bin` without the extension,
33+
so `gh-prs-merge` is a real command. (Not named `link` — that is a pnpm builtin,
34+
and `pnpm link` would run pnpm's own command instead of this one.) Make sure the
35+
directory is on `PATH`:
3636

37-
```bash
38-
node scripts/install-links.mjs --dry-run --force # see what would move
39-
node scripts/install-links.mjs --force # take them over
37+
```sh
38+
export PATH="$HOME/.local/bin:$PATH"
4039
```
4140

42-
`--force` takes over a *symlink*. A real file of the same name is still
43-
refused — clobbering someone's actual binary to install a convenience is not a
44-
trade a script gets to make on its own.
41+
### Migrating from `profullstack/scripts`
42+
43+
These names already exist in `~/.local/bin` pointing at `~/scripts/bin`, so a
44+
plain `pnpm link:bin` reports them as not-ours and changes nothing. To take them
45+
over:
46+
47+
```sh
48+
node scripts/install-links.mjs --dry-run --force # see exactly what would move
49+
node scripts/install-links.mjs --force # do it
50+
```
51+
52+
`--force` replaces a *symlink*. A real file of the same name is still refused —
53+
clobbering someone's actual binary to install a convenience is not a trade a
54+
script gets to make on its own.
4555

4656
To go back:
4757

48-
```bash
49-
pnpm unlink # remove the ones we own
58+
```sh
59+
pnpm unlink:bin # remove ours
5060
ln -sf ~/scripts/bin/gh-prs-merge ~/.local/bin/gh-prs-merge # and so on
5161
```
5262

53-
## Files on PATH, not shell functions
63+
## Usage
5464

55-
These install as executables on PATH rather than shell aliases or functions.
65+
### `gh-prs`
5666

57-
The older tools carry a comment saying this is because the moshcode pit runs
58-
aliases with `zsh -c`, a non-interactive shell that reads neither `~/.zshrc` nor
59-
`~/.zsh_aliases`. **That is no longer true**`src/aliases.mjs` in current
60-
moshcode runs `$SHELL -ic`, which is interactive and does source them. Verified:
67+
Lists open pull requests across any number of organizations and personal
68+
accounts, newest first, as an aligned table. In a capable terminal the PR number
69+
and URL become clickable.
6170

62-
```console
63-
$ zsh -ic 'gh-prs-all --help' # works — the pit's path
64-
$ zsh -c 'gh-prs-all --help' # zsh:1: command not found
71+
```sh
72+
gh-prs --orgs profullstack,moshcoder,h4kr,infernetprotocol
73+
gh-prs --users ralyodio
74+
gh-prs --orgs profullstack --users ralyodio --limit 50
75+
gh-prs --orgs profullstack --no-links # plain text, for piping
6576
```
6677

67-
The reason to stay on PATH is the weaker but still sufficient one: a file works
68-
from every caller — an interactive shell, `zsh -c`, a systemd unit, a CI step —
69-
without anything having been sourced first. A shell alias only works where a
70-
startup file was read.
78+
### `gh-prs-merge`
7179

72-
Nothing should alias *to* these either. A function beats PATH, so a wrapper of
73-
the same name silently shadows the file and the two drift apart.
74-
75-
Pit aliases (`/alias set <name> "<command>"`, stored in
76-
`~/.moshcode/aliases.json`):
80+
Walks the same scopes and squash-merges every PR that qualifies, oldest first.
81+
**Dry run by default** — nothing changes until you pass `--apply`.
7782

83+
```sh
84+
gh-prs-merge --orgs profullstack # report only
85+
gh-prs-merge --orgs profullstack --apply # merge what qualifies
86+
gh-prs-merge --orgs profullstack --apply --fix # repair, then merge
87+
gh-prs-merge --orgs profullstack --apply --fix --fix-wait 900
7888
```
79-
/alias set prs "gh-prs --orgs profullstack"
80-
/alias set merge "gh-prs-merge --orgs profullstack --apply --fix"
81-
/alias set merge-dry "gh-prs-merge --orgs profullstack"
82-
/alias set fixprs "gh-prs-fix-all"
83-
/alias set feed "tcfeed"
84-
/alias set whoisj "domainjson"
85-
```
8689

87-
## `gh-prs-merge --fix`
90+
A PR is merged only when all of these hold:
91+
92+
- it is open, and not a draft (or was successfully marked ready)
93+
- `mergeable` is `MERGEABLE` and `mergeStateStatus` is `CLEAN`
94+
- at least one CI check exists, unless `--allow-no-checks`
95+
- every check is `pass` or `skipping`
96+
- the head commit has not changed when the merge is submitted
97+
98+
That last one is the safety property. Between reading the checks and submitting
99+
the merge, someone can push; `--match-head-commit` means the merge lands on the
100+
commit that was actually verified or not at all. There is deliberately no
101+
`--admin`, so branch protections stay enforced.
102+
103+
#### `--fix`
88104

89-
A skip is not always a verdict on the PR. Two PRs were once skipped as
90-
`mergeStateStatus=UNSTABLE` purely because a check had not reported yet; nothing
91-
was wrong with either, and both merged unchanged minutes later.
105+
A skip is not always a verdict on the PR. Two were once skipped as
106+
`mergeStateStatus=UNSTABLE` purely because a check had not reported yet
107+
nothing was wrong with either, and both merged unchanged minutes later.
92108

93109
`--fix` repairs a repairable skip **once**, then judges the PR again against the
94110
identical rules. It requires `--apply`, because every repair writes.
@@ -107,26 +123,94 @@ What it will not do is as much of the design:
107123
reviewed.
108124
- **A check that ran and failed is a result, not an obstacle.** Retrying until it
109125
passes is how a flaky suite becomes a green one that means nothing.
110-
- **No `--admin`.** Branch protections stay enforced.
111126

112-
## Nothing under `bin/` does work at import time
127+
### `gh-prs-fix-all`
113128

114-
Every entry point guards its side effects with `isMain(import.meta.url)`, and
115-
anything worth testing lives in `src/`.
129+
Looks at every open threatcrush-scan pull request and fixes the ones broken
130+
because of us. Reports the rest and leaves them alone.
116131

117-
This is not decorative. A test that imported `bin/gh-prs-fix-all.ts` to reach one
118-
pure function *ran the tool*: the suite went from 60ms to 93 seconds and swept
119-
live pull requests with `--fix` implied. The guard and the `src/` split are both
120-
that lesson.
132+
```sh
133+
gh-prs-fix-all # fix ours, report theirs
134+
gh-prs-fix-all --dry-run # change nothing, just say what stands
135+
gh-prs-fix-all owner/name ... # only these
136+
```
137+
138+
The name says fix-all and it will not fix all, deliberately. Pushing to a fork
139+
sets off whatever the upstream repo runs on push, so their suite goes red
140+
against a commit that only added files under `.github/`. Those are reported,
141+
never touched.
142+
143+
### `tcfeed`
144+
145+
```sh
146+
tcfeed # the 50 newest posts
147+
tcfeed 100 # more of them
148+
tcfeed --forget # look at everything again next time
149+
tcfeed pr owner/name [--dry-run] # install the scan workflow
150+
tcfeed check [--fix] # how are the open requests doing
151+
```
152+
153+
The scanner itself lives in the threatcrush checkout, so this is a launcher.
154+
Point it elsewhere with `TCFEED_REPO`; every other `TCFEED_*` variable is read
155+
by the script it launches and works unchanged.
121156

122-
The `realpath` in `isMain` matters too — these install as symlinks, so
123-
`process.argv[1]` is the link while `import.meta.url` is its target. Comparing
124-
them raw reports "imported" for every installed command, disabling all of them at
125-
once.
157+
### `domainjson`
158+
159+
One JSON object on stdout: `{ name, rdap | moshpit, dns }`.
160+
161+
```sh
162+
domainjson example.com
163+
domainjson --name example.com
164+
domainjson --registry https://pit.moshcode.sh --timeout 4000 example.hacker
165+
domainjson -s https://rdap.example example.com # OpenRDAP flags pass through
166+
```
167+
168+
Names ending in a Moshpit TLD are served from the registry API; everything else
169+
goes through OpenRDAP. Either way `dig` adds records, hosts, reverse lookups and
170+
per-nameserver AXFR attempts. Errors are JSON too — a tool whose output gets
171+
parsed should not change shape when it fails.
172+
173+
## Aliases
174+
175+
Pit aliases live in `~/.moshcode/aliases.json`:
176+
177+
```
178+
/alias set prs "gh-prs --orgs profullstack"
179+
/alias set merge "gh-prs-merge --orgs profullstack --apply --fix"
180+
/alias set merge-dry "gh-prs-merge --orgs profullstack"
181+
/alias set fixprs "gh-prs-fix-all"
182+
/alias set feed "tcfeed"
183+
/alias set whoisj "domainjson"
184+
185+
/alias # list
186+
/alias get merge # show one
187+
/alias rm merge # forget one
188+
```
189+
190+
Arguments append rather than substitute, so `/merge --limit 5` works.
191+
192+
### Why these are files on `PATH`
193+
194+
The older tools carry a comment saying it is because the moshcode pit runs
195+
aliases with `zsh -c`, a non-interactive shell that reads neither `~/.zshrc` nor
196+
`~/.zsh_aliases`. **That is no longer true** — current moshcode runs
197+
`$SHELL -ic`, which is interactive and does source them:
198+
199+
```console
200+
$ zsh -ic 'gh-prs-all --help' # works — the pit's actual path
201+
$ zsh -c 'gh-prs-all --help' # zsh:1: command not found
202+
```
203+
204+
The reason to stay on `PATH` is the weaker but sufficient one: a file works from
205+
every caller — an interactive shell, `zsh -c`, a systemd unit, a CI step —
206+
without anything having been sourced first.
207+
208+
Nothing should alias *to* these either. A function beats `PATH`, so a wrapper of
209+
the same name silently shadows the file and the two drift apart.
126210

127211
## Development
128212

129-
```bash
213+
```sh
130214
pnpm test # vitest
131215
pnpm typecheck # tsc --noEmit
132216
```
@@ -135,11 +219,33 @@ Tests stub the subprocess layer rather than the network, so `gh` is never
135219
invoked. The suite runs in well under a second; if it starts taking longer,
136220
something is reaching the network that should not be.
137221

138-
## Differences from the originals
222+
**Nothing under `bin/` does work at import time.** Every entry point guards its
223+
side effects with `isMain(import.meta.url)`, and anything worth testing lives in
224+
`src/`. That is not decorative: a test that imported `bin/gh-prs-fix-all.ts` to
225+
reach one pure function *ran the tool*, taking the suite from 60ms to 93 seconds
226+
and sweeping live pull requests with `--fix` implied.
227+
228+
`isMain` resolves the realpath first, because these install as symlinks —
229+
`process.argv[1]` is the link while `import.meta.url` is its target, and
230+
comparing them raw reports "imported" for every installed command at once.
231+
232+
## Why TypeScript
139233

140-
Deliberate, and small:
234+
Ported from the bash and JavaScript originals in
235+
[`profullstack/scripts`](https://github.com/profullstack/scripts). The point was
236+
not the language. It was the two things bash was making expensive:
237+
238+
- **Typed, validated responses.** Every `gh` call went through `jq -r` into a
239+
string compare. `jq -r '.mergeable'` on a response that never had the field
240+
prints the four characters `null`, which is not `MERGEABLE` — so a perfectly
241+
mergeable PR read as ineligible *for a reason nobody wrote*, indistinguishable
242+
from a real verdict. An unrecognised field is now named in an error.
243+
- **Tests.** The originals had none, so verifying a change meant running it
244+
against live pull requests.
245+
246+
## Differences from the originals
141247

142248
- `gh-prs` prints `No open PRs found.` instead of a bare header row.
143249
- `gh-prs-merge` adds `fixed=` to its summary line.
144-
- `domainjson` output is unchanged in structure; DNS answers arrive in
145-
round-robin order, so array ordering varies between runs of either version.
250+
- `domainjson` is unchanged in structure. DNS answers arrive round-robin, so
251+
array ordering varies between runs of either version.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
"test": "vitest run",
1717
"test:watch": "vitest",
1818
"typecheck": "tsc -p tsconfig.json --noEmit",
19-
"link": "node scripts/install-links.mjs",
20-
"unlink": "node scripts/install-links.mjs --remove"
19+
"//link": "Not named `link`/`unlink`: those are pnpm builtins, and a bare `pnpm link` runs pnpm's own command rather than this script.",
20+
"link:bin": "node scripts/install-links.mjs",
21+
"unlink:bin": "node scripts/install-links.mjs --remove"
2122
},
2223
"devDependencies": {
2324
"@types/node": "^22.10.2",

0 commit comments

Comments
 (0)