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
3028pnpm 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
4656To go back:
4757
48- ``` bash
49- pnpm unlink # remove the ones we own
58+ ``` sh
59+ pnpm unlink:bin # remove ours
5060ln -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
94110identical 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
130214pnpm test # vitest
131215pnpm typecheck # tsc --noEmit
132216```
@@ -135,11 +219,33 @@ Tests stub the subprocess layer rather than the network, so `gh` is never
135219invoked. The suite runs in well under a second; if it starts taking longer,
136220something 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.
0 commit comments