Port Contract
This doc is the source of truth for what a port is in this repo, what every port must contain, and how to add a new one. It exists because ports/ has historically carried several different meanings (packages, fixtures, build staging), and Waves 2–6 of the ports-ontology cleanup need one reference to conform to.
Target length: ~2 pages. If you came here to browse: read the invariant, the template, and the 10-step checklist.
Invariant
A subdirectory of
ports/is a self-contained adaptation of third-party software for the fishnet runtime. It is NOT a workspace package, NOT a build output, and NOT a test fixture. Each port shipssrc/(source),build.sh(recipe),package.json(contract),index.ts(TS wrapper), and its compiled artifact — depending on nothing inports/outside itself.
Current roster
As of Wave 0 (2026-04-21), ports/ contains four entries that qualify as ports under the invariant:
ports/vim/— Vim editor (Emscripten, raw-TTY)ports/jaq/— jq-like JSON processor (WASI, Rust)ports/tree/—tree(1)directory lister (WASI)ports/micro-editor/— minimal C editor (Emscripten, raw-TTY)
All four ports conform to the full template as of Wave 6 (2026-04-21): each owns its src/, build.sh, package.json, index.ts, LICENSE, and committed artifact. The historical staging directories ports/wasm-builds/ and ports/wasi-builds/ no longer exist — Rust/C sources live alongside their consumer (per-port for the four real ports; per-fixture under test/fixtures/wasi/test-<name>/src/ for WASI test fixtures).
Template
The canonical on-disk layout for a port:
ports/<name>/ src/ source tree (Cargo.toml + src/*.rs for Rust, *.c for C, etc.) lib/ pre-built static deps NOT produced by build.sh (optional) build.sh executable recipe; cwd = port dir; produces artifact at root package.json name, version, scripts.build, upstream block, fishbowl block index.ts TS wrapper (createWasiBin / createWasmBin) <name>.wasm compiled artifact (committed until a later LFS migration) LICENSE copy of upstream licenseContributors start by copying one of two canonical templates:
- WASI reference —
ports/jaq/(pure Rust compiled towasm32-wasip1, post-processed withwasm-opt --asyncify). - Emscripten reference —
ports/micro-editor/(simple C compiled throughemccwith Asyncify for raw-TTY).
Pick the template whose runtime matches what you’re porting. Do not invent a third layout; the invariant is there so CI can iterate ports/* uniformly.
Extension: large upstream trees via submodule
Ports that adapt a large upstream tree (vim, emacs-shaped projects, anything where vendoring 100k+ lines of source as regular tracked files would balloon the monorepo) MAY have src/ BE a git submodule pointing at the upstream repo. In that case, in-house additions (shim sources, glue code, patches) go in shims/ as a sibling of src/, not nested inside it — the submodule is read-only from our perspective, and our additions need a writable home outside it. Pre-built artifacts produced from those shims still live in lib/. Worked example: ports/vim/ (Wave 3). Three peers under the port root: src/ (upstream submodule), shims/ (our sources), lib/ (pre-built libs).
Build contract
Every port’s package.json declares its build recipe identically:
{ "scripts": { "build": "bash build.sh" }}The literal string "bash build.sh" is required so CI and grep can rely on a single uniform verb. package.json#scripts.build is the mount point; build.sh is the server behind it. CI invokes pnpm --dir ports/<p> run build, which resolves to bash build.sh.
build.sh must:
- Exit 0 on success.
- Produce every file listed in
package.json#fishbowl.assetsat the port root (e.g.<name>.wasm,<name>.mjsfor Emscripten). - Be idempotent — re-running on clean or dirty inputs yields the same artifacts.
- Precondition-check its compiler (
command -v cargo,command -v emcc) and fail with a clear error message if it’s missing, rather than emitting a cryptic compiler failure. - Live at the port root with
cwd = "$(dirname "$0")"so it can be invoked from anywhere in the tree.
Note: the "bash build.sh" mandate is here for uniformity at four ports, not as doctrine. If a future pure-TS or pure-cargo port has real reason to skip the bash shim, revisit this clause rather than wrapping build.ts in a one-line shim to satisfy grep.
Rules
Plain-prose rules that follow from the invariant. Waves 2+ enforce these through path fixes and moves; future CI work may add mechanical checks.
-
Self-contained. A port depends on nothing in
ports/outside its own directory. It does notimport(or#include, orsource) from any sibling port. -
Third-party. A port adapts software that originates outside this repo. Internal-only components (product features, test harnesses, shared test utilities) live in
packages/ortest/, notports/. -
One identifier. A port’s directory name, its
package.json#name(minus the@fishnet/scope), and itsregistryNameinregistry.config.json(if registered) all agree.ports/jaq/↔@fishnet/jaq↔"registryName": "jaq". -
One primary artifact. Each port produces exactly one primary artifact, declared in
package.json#fishbowl.assets. Secondary files (Emscripten glue.mjs, upstream license) are included as supporting assets, not co-equal outputs. -
Upstream pinned in
package.json. Origin, version, and license of the adapted software go inpackage.json#upstreamas a structured object — parseable for future license-audit / version-drift checks — not in a free-formUPSTREAM.txt."upstream": {"source": "https://github.com/vim/vim","version": "v9.1.0","license": "Vim"} -
LICENSE file. Each port ships a
LICENSEfile copying the upstream license verbatim.package.json#upstream.licenseis the SPDX identifier;LICENSEis the legal text. -
Retirement is deletion. Retiring a port means deleting
ports/<name>/and removing its entry fromregistry.config.jsonin a single commit. Noports/_archive/— git history is the archive.
Distribution pipeline
How a port reaches a user’s shell via pkg install:
ports/<name>/ ports/<name>/package.json src/, build.sh, index.ts, <name>.wasm has fishbowl.{type,bins,assets} │ │ │ pnpm --dir ports/<name> run build │ ▼ │ artifacts committed at ports/<name>/root │ │ │ ▼ │ registry.config.json ◄── port declared by name/registryName ───┘ │ │ pnpm run registry:rebuild ▼ packages/registry-tools/ (TS: @fishnet/registry-tools) │ esbuild bundles index.ts + copies flat assets ▼ registry/ registry/index.json index.json, packages/<name>/bundle.mjs + assets │ │ HTTP fetch (Node/browser) ▼ pkg install <name> user shell promptKey references:
packages/registry-tools/— first-class workspace package; stays put.registry.config.json— root-level, consumed bypnpm run registry:rebuild.registry/— generated output (currently sibling to source; may migrate underdist/in a follow-up).
Adding a port — 10-step checklist
Copy-pasteable commands for adding a new port <name>. Replace <name> and adjust the source/license stanzas. The commands below assume the post-Wave-6 layout — every port owns its own src/ tree at ports/<name>/src/.
# 1. Copy the canonical template for your runtime.# WASI/Rust:cp -r ports/jaq ports/<name># Emscripten/C:cp -r ports/micro-editor ports/<name>
# 2. Update package.json: name, version, upstream, and fishbowl blocks.# Replace every occurrence of the template's name. Set upstream.source /# upstream.version / upstream.license to the adapted software's origin.$EDITOR ports/<name>/package.json
# 3. Confirm scripts.build is the literal "bash build.sh". Do not rename.jq -r '.scripts.build' ports/<name>/package.json# → "bash build.sh"
# 4. Swap in upstream source under src/. Clean out any template leftovers.rm -rf ports/<name>/srcgit clone --depth 1 --branch <version-tag> <upstream-url> ports/<name>/src# Or: extract a tarball, vendor a subset, etc.
# 5. Adapt build.sh to invoke your compiler against src/. Keep the# `cd "$(dirname "$0")"` guard and preserve `set -euo pipefail`.$EDITOR ports/<name>/build.sh
# 6. Build once and verify the artifact lands at the port root.cd ports/<name> && bash build.sh && cd -ls ports/<name>/<name>.wasm
# 7. Write index.ts: pick createWasiBin (WASI) or createWasmBin (Emscripten),# keep the `(resolver?: AssetResolver)` signature, export a testSpec.$EDITOR ports/<name>/index.ts
# 8. Copy the upstream LICENSE file into the port root verbatim.cp <upstream-license-source> ports/<name>/LICENSE
# 9. (Optional, if you want the port installable via `pkg install`.)# Register in registry.config.json — name, directory, and registryName# must all agree. Then rebuild the registry.$EDITOR registry.config.jsonpnpm run registry:rebuild
# 10. Commit — include src/, build.sh, package.json, index.ts, LICENSE,# and the compiled <name>.wasm artifact in a single commit.git add ports/<name>git commit -m "ports(<name>): adapt <upstream@version>"After commit, run the relevant test suite (pnpm run test:wasm for WASM/WASI ports) to confirm your testSpec passes.