After I was constructing SwiftBash I made surprisingly fast headway on the fundamental CLI utilities — jq, awk, sed, grep. Each is a small, well-scoped language, and when you sit down with the spec it truly is only a parser and an evaluator.
Then I hit a wall. The 2 CLIs I attain for many as a working developer aren’t tiny languages — they’re gh and glab, the GitHub and GitLab purchasers. And proper subsequent to them, the granddaddy of all dev CLIs: git. These aren’t 2,000-line instruments. gh alone is roughly fifty thousand strains of Go, with subcommand bushes, OAuth flows, REST + GraphQL purchasers, pagination, archive extraction, jq filtering — the works. Reimplementing all of that by hand felt like a 12 months of evenings.
However the supply code is correct there on GitHub. And I’ve a coding agent. So I started to surprise: shouldn’t Opus 4.7 1M (extra-high) have the ability to translate cli/cli into Swift for me, given the unique as floor fact?
It seems: sure. That’s the place SwiftPorts comes from.
What “porting” truly seems to be like
The workflow is duller than it sounds. I level the agent at a subcommand within the upstream Go supply, give it the present Swift module’s conventions, and ask it to provide the equal below swift-argument-parser. Then I run the ensuing binary side-by-side with the unique device and search for divergence: completely different exit codes, completely different output framing, lacking flags, flawed default behaviour.
A working instance from this morning: a Codex reviewer caught that our gh api --input was silently dropping -f/-F area flags, the place upstream paperwork that these flags must be appended to the endpoint’s question string. Two-line bug, one acceptance take a look at, merged. That type of paper-cut parity bug is your entire sport. Construct the floor, then beat it towards actuality till it behaves identically.
I began with gh and thru a couple of iterations acquired a lot of the helpful operations working: repo, pr, challenge, launch, workflow, run, gist, challenge, label, org, cache, variable, secret, ssh-key, gpg-key, search, config, auth. Some obscure corners — like gh attestation — I left for later. For those who genuinely use gh attestation in anger, please inform me what it’s good for.
Subsequent was glab for GitLab (which I run self-hosted), and a sample emerged: lots of the host-agnostic plumbing — TTY detection, ANSI dealing with, the keychain wrapper, the abstraction over git for the clone-and-checkout dance — was duplicated between the 2. So we factored that out into ForgeKit, which each GhCommand and GlabCommand now share.
The git-aware ones, and SwiftGit on libgit2
There’s a category of gh/glab operations that aren’t pure distant API calls — they’re “git-aware.” gh pr create must know your present department and the distant’s proprietor/identify. gh repo clone shells out to git clone. gh pr checkout does a git fetch adopted by git checkout. glab mr checkout is similar form.
Upstream gh solves this by actually invoking /usr/bin/git as a subprocess. That works, nevertheless it’s not embeddable: no Course of on iOS, no system git binary in a sandboxed Mac App Retailer app.
Some time again I had some profitable experiments with libgit2, the C reimplementation of git. So I puzzled: might the agent construct a whole git shopper on high of it? And — sure, it might. A lot of the work, it seems, is mechanical wiring: take an ArgumentParser flag, map it to the corresponding git_* possibility struct area, hand the struct to libgit2, translate the consequence again. There are sharp edges round credential callbacks, signature decision, and the per-op git_libgit2_opts international state, however the bulk of git init / clone / fetch / pull / push / standing / log / diff / present / commit / merge / rebase / cherry-pick / reset / checkout / swap / restore / add / rm / mv / clear / stash / tag / department / distant / config / rev-parse / ls-files / ls-tree / cat-file / describe / blame / apply / reflog is simply plumbing.
That grew to become the SwiftGit module, with its personal git executable. ForgeKit’s GitClient protocol lets gh/glab swap between a ProcessGitClient (the “shell out” path) and SwiftGit’s libgit2-backed in-process shopper with out altering a line of subcommand code. On iOS, SwiftGit is the one path that works.
The compression rabbit gap
A handful of gh operations want decompression. gh launch obtain ought to auto-extract .zip / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .tar.lz4 property with out subprocess calls; gh run obtain and gh run view --log have to crack open ZIP-format workflow artifacts.
I began with ZIPFoundation, and that labored fantastically — till I attempted to construct for Android. (I now routinely construct for Android and Home windows in CI, as a result of nothing focuses the thoughts like 4 inexperienced checkmarks throughout 4 platforms.)
Marc Prud’hommeaux dropped an ideal suggestion in challenge #6: use his Swift-friendly fork of libarchive as a substitute of ZIPFoundation. I had Opus consider it, and the conclusion was: this modifications the scope. libarchive doesn’t simply offer you Zip — it offers you tar with auto-detected gzip / bzip2 / xz / zstd / lz4 filtering, plus 7z, plus a half-dozen different codecs no one’s asking for. So as a substitute of 1 ZipKit umbrella we ended up with a complete compression household: ZipKit, TarKit, GzipKit, Bzip2Kit, XzKit, ZstdKit, Lz4Kit. Each ships its personal package (the library) plus a command (the CLI), plus the one-letter aliases (gunzip, zcat, bunzip2, xzcat, unzstd, lz4cat, …) you’d discover in coreutils.
The enjoyable edge case: not each codec ships as a system library on iOS. liblzma and liblz4 specifically aren’t individually out there there. So these kits have a look at the platform at compile time and route by means of Apple’s Compression framework as a substitute — XzKit makes use of Compression.framework‘s LZMA path; Lz4Kit makes use of COMPRESSION_LZ4_RAW. The result’s that an iOS app can gh launch obtain a .tar.xz asset and unpack it solely in-process, with no subprocess and no missing-codec apology.
JqKit, stolen from SwiftBash
gh api --jq and glab api --jq are the way you truly use these instructions productively towards GraphQL. Upstream gh runs the response by means of an actual jq library; the lazy port would shell out to /usr/bin/jq.
I’d already written a pure-Swift jq parser + evaluator + builtins for SwiftBash, so I stole it again and wrapped it in JqKit — a Jq.eval / Jq.evalString facade with no system C dependency, callable from any Swift context. gh api --jq '.full_name' now runs the filter in-process. So does glab api --jq.
That is the a part of SwiftPorts that’s genuinely mutual with SwiftBash. Each tasks profit; neither owns the code.
Sandboxing and async all the pieces
As soon as the floor space was vast sufficient, I began prep work for plugging these instruments into SwiftBash. That meant two massive mechanical refactors.
The primary was making all the pieces async-throwing and including Process.checkCancellation() calls inside each sizzling loop — the recursive listing walks in tar, the per-page pagination loops in gh search, the byte-pump loops within the compression engines. The user-facing payoff is that SwiftBash can Ctrl-C a operating operation and have it truly cease in milliseconds, as a substitute of ready out the remainder of a 50,000-file stroll.
The second was a sandbox. SwiftBash, when embedded in an iOS or sandboxed-Mac app, can’t run with the host’s full filesystem and setting — it must be confined to a folder, with setting variables and argv strictly below the embedder’s management.
What I actually needed was for the OS to offer me a sandbox primitive I might simply enter. macOS has sandbox_init and Apple’s seatbelt profiles, however they’re personal API and never what you wish to be delivery on the App Retailer. iOS doesn’t expose something comparable in any respect. So I needed to construct it in person house.
The result’s the Sandbox module, about 650 strains of Swift in two recordsdata. It’s a default-deny @TaskLocal coverage: when Sandbox.present is non-nil, each URL handed to the gated I/O websites in SwiftPorts has to authorize by means of Sandbox.authorize(_:), and each ambient attain (setting variables, course of arguments, area directories like ~/.config) consults the sandbox’s personal values relatively than the host’s. Two factories cowl the widespread circumstances: Sandbox.rooted(at:) for single-folder confinement on Mac/Linux, and Sandbox.appContainer(id:) for iOS the place the usual paperwork/short-term/caches/group folders kind the pure perimeter.
Crucially, the sandbox additionally intercepts setting variable reads. The naïve sandbox is the one you possibly can escape with HOME=/and so on git config --global ... — level a device at a “completely different” house listing and watch it write exterior your perimeter. SwiftPorts code by no means reads ProcessInfo.processInfo.setting straight; it reads by means of the sandbox, which by default returns [:] and solely returns host values if the embedder explicitly asks for passthrough. I went by means of each current name website with a regression take a look at that bans ambient ProcessInfo and FileManager entry in Sources/, so the perimeter doesn’t bit-rot.
When Sandbox.present is nil — which is the case for everybody operating the binaries straight from the command line — each gate is a no-op and behavior is similar to the unique. You solely pay for the sandbox for those who’re embedding.
So… the place does this all dwell?
SwiftPorts is a separate repo from SwiftBash on function. Growing the dev tooling individually retains every tractable and lets the ports be used as true CLIs, or embedded into apps with out dragging in an interpreter. You might, for instance, construct an actual GitHub shopper for iPad on high of GitHub + SwiftGit + JqKit and by no means want SwiftBash in any respect.
I’m nonetheless determining the place the road is. My present feeling: something that’s a port of an actual CLI utility belongs in *-Ports. SwiftBash must be simply the interpreter — the bash language, expansions, redirections, management circulation — pulling in builtins by Swift Bundle dependency. SwiftScript, the Swift interpreter, matches the identical form: one other language frontend that consumes the identical builtins.
If that lands, the image I see is a pull-down bash shell on my iPad with a coding agent within the subsequent pane, absolutely sandboxed and App Retailer-legal. SwiftBash for the shell, SwiftScript for the inline-Swift-snippet escape hatch, SwiftPorts for the precise work — git, gh, tar, jq. That’s the unifying daydream.
Proper now SwiftPorts continues to be very a lot an answer looking for an issue. I’ve a nebulous imaginative and prescient of a command-center app that reads points from GitLab and GitHub, fingers them to coding brokers, offers with overview feedback, watches CI, and fixes issues that come up there — a common Mac/iOS app I might hold operating on my iPad to babysit my OSS whereas AFK. Perhaps that’s the place this goes.
What I really need from you
All three tasks — SwiftPorts, SwiftBash, SwiftScript — are largely in need of actual use circumstances that train them towards the originals. The aim is for the ported utilities to behave precisely just like the instruments they change. For those who run gh or glab or git from SwiftPorts and you notice something — a flag the upstream device accepts that ours rejects, an output format that’s subtly completely different, a default that diverges, an exit code that doesn’t match — that’s the gold. Open a difficulty and I’ll feed it to the agent. The nearer we get to invisible parity, the extra helpful any of this turns into.
The repo is at github.com/Cocoanetics/SwiftPorts. Inform me what you’d construct with it — and inform me the place it will get issues flawed.
Associated
Classes: Updates

