After I was constructing SwiftBash I made surprisingly fast headway on the essential 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 shoppers. 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 traces of Go, with subcommand bushes, OAuth flows, REST + GraphQL shoppers, 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 marvel: shouldn’t Opus 4.7 1M (extra-high) be capable 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 like
The workflow is duller than it sounds. I level the agent at a subcommand within the upstream Go supply, give it the prevailing Swift module’s conventions, and ask it to supply the equal underneath swift-argument-parser. Then I run the ensuing binary side-by-side with the unique software and search for divergence: completely different exit codes, completely different output framing, lacking flags, improper default behaviour.
A working instance from this morning: a Codex reviewer caught that our gh api --input was silently dropping -f/-F subject flags, the place upstream paperwork that these flags ought to be appended to the endpoint’s question string. Two-line bug, one acceptance take a look at, merged. That form of paper-cut parity bug is your complete recreation. Construct the floor, then beat it in opposition to actuality till it behaves identically.
I began with gh and thru a number of iterations obtained a lot of the helpful operations working: repo, pr, situation, launch, workflow, run, gist, mission, label, org, cache, variable, secret, ssh-key, gpg-key, search, config, auth. Some obscure corners — like gh attestation — I left for later. In the event you 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: a number 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/title. 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 an entire git consumer on prime 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_* choice struct subject, hand the struct to libgit2, translate the end result 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 turned 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 consumer 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 must crack open ZIP-format workflow artifacts.
I began with ZIPFoundation, and that labored superbly — 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 5 inexperienced checkmarks throughout iOS, Mac, Linux, Home windows and Android)
Marc Prud’hommeaux dropped an awesome suggestion in situation #6: use his Swift-friendly fork of libarchive as a substitute of ZIPFoundation. I had Opus consider it, and the conclusion was: this adjustments the scope. libarchive doesn’t simply offer you Zip — it provides you tar with auto-detected gzip / bzip2 / xz / zstd / lz4 filtering, plus 7z, plus a half-dozen different codecs no person’s asking for. So as a substitute of 1 ZipKit umbrella we ended up with an entire compression household: ZipKit, TarKit, GzipKit, Bzip2Kit, XzKit, ZstdKit, Lz4Kit. Each ships its personal equipment (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 take a look at the platform at compile time and route by way 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 completely 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 in opposition to GraphQL. Upstream gh runs the response by way 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 initiatives profit; neither owns the code.
Sandboxing and async the whole lot
As soon as the floor space was huge sufficient, I began prep work for plugging these instruments into SwiftBash. That meant two large mechanical refactors.
The primary was making the whole lot async-throwing and including Job.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 working 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 atmosphere — it needs to be confined to a folder, with atmosphere variables and argv strictly underneath the embedder’s management.
What I actually needed was for the OS to supply 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 need to be transport 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 traces of Swift in two information. 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 way of Sandbox.authorize(_:), and each ambient attain (atmosphere variables, course of arguments, area directories like ~/.config) consults the sandbox’s personal values somewhat than the host’s. Two factories cowl the frequent circumstances: Sandbox.rooted(at:) for single-folder confinement on Mac/Linux, and Sandbox.appContainer(id:) for iOS the place the usual paperwork/non permanent/caches/group folders type the pure perimeter.
Crucially, the sandbox additionally intercepts atmosphere variable reads. The naïve sandbox is the one you’ll be able to escape with HOME=/and many others git config --global ... — level a software at a “completely different” dwelling listing and watch it write exterior your perimeter. SwiftPorts code by no means reads ProcessInfo.processInfo.atmosphere straight; it reads by way of the sandbox, which by default returns [:] and solely returns host values if the embedder explicitly asks for passthrough. I went by way of each current name web site 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 working the binaries straight from the command line — each gate is a no-op and behavior is an identical to the unique. You solely pay for the sandbox should you’re embedding.
So… the place does this all reside?
SwiftPorts is a separate repo from SwiftBash on goal. Creating 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 could possibly, for instance, construct an actual GitHub consumer for iPad on prime 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 ought to be simply the interpreter — the bash language, expansions, redirections, management circulation — pulling in builtins by Swift Bundle dependency. SwiftScript, the Swift interpreter, suits 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, totally 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 in the hunt 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 assessment feedback, watches CI, and fixes issues that come up there — a common Mac/iOS app I might hold working on my iPad to babysit my OSS whereas AFK. Possibly that’s the place this goes.
What I really need from you
All three initiatives — SwiftPorts, SwiftBash, SwiftScript — are principally in need of actual use circumstances that train them in opposition to the originals. The objective is for the ported utilities to behave precisely just like the instruments they substitute. In the event you run gh or glab or git from SwiftPorts and you notice something — a flag the upstream software 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 problem 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 on GitHub. Inform me what you’d construct with it — and inform me the place it will get issues improper.
Associated
Classes: Updates
