Skip to content

feat: port the shell tools to TypeScript, with types and tests - #2

Merged
ralyodio merged 2 commits into
masterfrom
feat/ts-clis
Aug 16, 2026
Merged

feat: port the shell tools to TypeScript, with types and tests#2
ralyodio merged 2 commits into
masterfrom
feat/ts-clis

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Ports all five commands out of profullstack/scripts: gh-prs, gh-prs-merge, gh-prs-fix-all, tcfeed, domainjson.

Why, given the originals worked

Not the language. 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, and the failure was indistinguishable from a real verdict. Responses are now parsed once and validated by shape, with the offending field named in the error.

Tests. The originals had none, so verifying a change meant running it against live pull requests. There are now 51, stubbing the subprocess layer so gh is never invoked.

What carried over unchanged

gh-prs-merge --fix keeps its behaviour and its refusals:

  • A conflict GitHub declines to merge is left alone, with its message printed as FIXME. Resolving one means choosing between two authors' intent.
  • A check that ran and failed is a result, not an obstacle.
  • No --admin. Branch protections stay enforced.

Two structural rules

Nothing under bin/ does work at import time. Every entry guards on isMain(import.meta.url); anything testable 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 — the suite went from 60ms to 93 seconds and swept live pull requests with --fix implied. I checked for damage (it only reported; nothing was pushed) and then fixed the shape rather than the symptom. The suite is back to 711ms, and that timing is itself the regression test.

isMain resolves the realpath first. 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.

Install

Commands install as files on PATH, never shell aliases — the moshcode pit runs aliases with zsh -c, which reads neither ~/.zshrc nor ~/.zsh_aliases, so a function defined there is simply not there.

pnpm install
node scripts/install-links.mjs --dry-run --force   # see what would move
node scripts/install-links.mjs --force             # take them over

The names already point at ~/scripts/bin, so a plain run reports them as not-ours and changes nothing. --force takes over a symlink; a real file of the same name is still refused.

Verification

  • 51 tests, 711ms. Covers eligibility rules, every --fix path, response validation, arg parsing, table padding, and time-ago thresholds.
  • pnpm typecheck clean under strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes.
  • Parity against the originals, run back to back:
    • gh-prs-merge dry run — byte-identical.
    • domainjsonset-identical across every record type, rdap key and axfr entry. Only array ordering differs, because DNS answers arrive round-robin; that varies between runs of either version.
    • gh-prs — one deliberate change: No open PRs found. instead of a bare header row.

Deliberate differences

  • gh-prs prints No open PRs found. rather than a lone header.
  • gh-prs-merge adds fixed= to its summary line.

🤖 Generated with Claude Code

Ports all five commands out of profullstack/scripts: gh-prs, gh-prs-merge,
gh-prs-fix-all, tcfeed and domainjson. The point is not the language. It
is the two things bash was making expensive.

Typed, validated responses. Every gh call went through `jq -r` into a
string compare, and `jq -r '.mergeable'` on a response that never had the
field prints the four characters `null` — which is not MERGEABLE, so a
mergeable PR read as ineligible for a reason nobody wrote, and the
failure was indistinguishable from a real verdict. Responses are now
parsed once and validated by shape, with the offending field named.

Tests. The originals had none, so verifying a change meant running it
against live pull requests. 51 tests here stub the subprocess layer, so
gh is never invoked and the suite finishes in under a second.

gh-prs-merge keeps the --fix behaviour and its refusals: a conflict
GitHub declines to merge is left alone with its message printed, a check
that ran and failed is a result rather than an obstacle, and there is no
--admin.

Two structural rules the port enforces:

  Nothing under bin/ does work at import time. Every entry guards on
  isMain(import.meta.url) and anything testable 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. These install as symlinks, so
  argv[1] is the link while import.meta.url is its target; comparing
  raw reports "imported" for every installed command at once.

Commands install as files on PATH rather than shell aliases, because the
moshcode pit runs aliases with `zsh -c`, which reads neither ~/.zshrc nor
~/.zsh_aliases. install-links refuses to take over a name it does not
own without --force, and refuses a real file even with it.

Verified against the originals: gh-prs-merge dry run is byte-identical,
and domainjson is set-identical across every record type, rdap key and
axfr entry (only DNS round-robin ordering differs).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@socket-security

socket-security Bot commented Aug 16, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedvitest@​4.1.10981007999100
Added@​types/​node@​22.20.11001008195100
Addedtsx@​4.23.121001008195100
Addedtypescript@​5.9.31001009010090

View full report

@socket-security

socket-security Bot commented Aug 16, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
License policy violation: npm typescript

License: LicenseRef-W3C-Community-Final-Specification-Agreement - The applicable license policy does not permit this license (5) (package/ThirdPartyNoticeText.txt)

From: package.jsonnpm/typescript@5.9.3

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/typescript@5.9.3. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

0 finding(s)

No findings.

Socket blocked the PR on GHSA-fx2h-pf6j-xcff (high) — vite's
`server.fs.deny` bypass on Windows alternate paths. vitest 2.1.9 pulled
in vite 5.4.21, and that range is only patched at 6.4.3. vitest 4.1.10
resolves vite 8.2.1, above the 8.0.16 fix.

Not exploitable here — it is a dev-server path check in a devDependency
of a repo with no dev server — but the fix is a version bump, and
arguing for an exception costs more than taking it. All 51 tests pass
unchanged on vitest 4.

Also corrects the README. It repeated the older tools' claim that the
moshcode pit runs aliases with a non-interactive `zsh -c`, which is why
they had to be files on PATH. That is no longer true: src/aliases.mjs
runs `$SHELL -ic`, which does source ~/.zsh_aliases. Verified both ways
and recorded the actual, weaker reason to stay on PATH — a file works
from every caller without anything having been sourced first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit d0f666a into master Aug 16, 2026
4 checks passed
@ralyodio
ralyodio deleted the feat/ts-clis branch August 16, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant