npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

jetbridge

v0.1.0-rc.2

Published

Build, sign and deliver iOS dev builds — to a simulator, down a cable, or over the air to your own bucket.

Readme

jetbridge

Build, sign and deliver iOS dev builds — to a simulator, down a cable, or over the air to your own bucket.

One config file, one command per destination, and no service in the middle: the .ipa goes to a bucket you own, and the phone installs from a link.

npm install --save-dev jetbridge

ContentsQuick start · Commands · Variants · Configuration · Credentials · Unattended runs · JSON output · Guides

Quick start

npx jetbridge init        # writes jetbridge.config.json by reading your Xcode project
npx jetbridge install     # builds and launches on a simulator

Start there deliberately: a simulator validates no code signature, so that second command needs no Apple credentials, no provisioning and no paired phone. Once it works, add credentials and move to a real device.

npx jetbridge install --device     # over the cable
npx jetbridge link                 # upload, and print a link anyone can install from

Commands

| Command | What it does | Needs Apple credentials | | ----------------------------- | ------------------------------------------------- | ----------------------- | | jetbridge init | write the config by inspecting your Xcode project | no | | jetbridge install | build and launch on a simulator | no | | jetbridge install --device | build and install over the cable | only with --suffix | | jetbridge link | archive, upload, print an install link | yes | | jetbridge bundles ls | what this project has registered with Apple | yes | | jetbridge bundles provision | refresh profiles without building | yes | | jetbridge bundles rm <slug> | remove named variants | yes | | jetbridge bundles prune | remove variants whose git branch is gone | yes | | jetbridge reindex | rebuild the build index page from the bucket | no |

The destination is a flag, not a separate command: --device means the cable, and --device "Dev's iPhone" means that one phone. Same for --simulator.

Deleting anything is a dry run by default — bundles rm and bundles prune print the plan and stop until you pass -y. Apple restores nothing.

Variants

Two branches installing dev builds overwrite each other, because both carry the same bundle id. A variant gives a branch its own bundle id and its own home-screen label, so both apps sit side by side and stay distinguishable.

jetbridge install --device --suffix seeded-feed
  → io.acme.app.dev.seededfeed, home-screen "Acme Seeded Feed"

jetbridge install --device --suffix
  → the same, named after the current git branch

Every variant registers a bundle id and a profile with Apple, permanently. jetbridge bundles prune clears out the ones whose branch has since been merged or deleted.

Configuration

jetbridge.config.json sits at the root of your project, and jetbridge init writes it for you. Editors autocomplete it from the $schema key.

{
  "$schema": "https://unpkg.com/jetbridge/schema/v1.json",
  "configVersion": 1,

  "project": {
    "iosDir": "ios",
    "scheme": "Acme",
    "teamId": "ABCDE12345",
    "baseBundleId": "io.acme.app.dev",
  },

  "variant": {
    "displayName": "Acme Dev",
    "targets": [{ "name": "Acme" }, { "name": "UploadWidget" }],
  },

  "credentials": {
    "ascKeyId": "${ASC_API_KEY_ID}",
    "ascIssuerId": "${ASC_API_ISSUER_ID}",
  },

  "ota": {
    "storage": {
      "bucket": "acme-builds",
      "region": "eu-central-1",
      "publicBaseUrl": "https://builds.example.com",
      "accessKeyId": "${JETBRIDGE_S3_ACCESS_KEY_ID}",
      "secretAccessKey": "${JETBRIDGE_S3_SECRET_ACCESS_KEY}",
    },
  },
}

Nothing secret belongs in this file. Every credential is a ${VAR} reference to the environment, never a value, so the config is safe to commit — and a bare "ASC_API_KEY_ID" is rejected rather than guessed at, because it reads as either a name or the secret.

Your project also has to consume a few build settings that jetbridge passes on the command line — appending the variant suffix to the bundle id, and pointing at the generated profile. The names are fixed: JETBRIDGE_SUFFIX, JETBRIDGE_DISPLAY_NAME, JETBRIDGE_SIGN_STYLE, JETBRIDGE_GIT_SHA, and one JETBRIDGE_PROFILE_<TARGET> per target. Those lines, and the reduced entitlements a variant needs, are in Adopting jetbridge.

Credentials

Registering a variant bundle id or minting a profile goes through the App Store Connect API, so those commands need an API key with the App Manager role. install on a simulator needs nothing, and a plain install --device signs against a profile you already have.

export ASC_API_KEY_ID=ABCD123456
export ASC_API_ISSUER_ID=69a6de70-0000-0000-0000-000000000000
# key file at ~/.appstoreconnect/private_keys/AuthKey_ABCD123456.p8

Making the key, where to put it, which role fails and why, and registering your device: Credentials.

Unattended runs

Every command works with nobody watching — a script, a pipe, CI, or an AI agent. You never need to set CI=1.

No terminal means no questions: jetbridge falls back to the flag-driven path and, where an answer is genuinely required, fails immediately naming the flag that supplies it. It never waits for input it cannot receive, because a command that hangs prints nothing at all.

jetbridge init --yes --scheme Acme --team-id ABCDE12345 | cat

--yes supplies consent: to writing the config, and to a deletion in bundles rm / bundles prune. If you are on a terminal and want the unattended path anyway, CI=1 and JETBRIDGE_NON_INTERACTIVE=1 both force it — neither is ever required.

JSON output

Every command takes --json. Machine output goes to stdout, one JSON object per line; every human line goes to stderr. So a script parses stdout while a person still watches the run.

jetbridge install --device --json | jq -c 'select(.event == "step" and .phase == "end")'
{"event":"start","command":"install","destination":"device","bundleId":"io.acme.app.dev.wave","displayName":"Acme Wave","configuration":"Debug","suffix":"wave"}
{"event":"devices","devices":[{"id":"10BFE051","name":"Dev's 17 Pro","udid":"…","transport":"wired"}]}
{"event":"signing","decision":"cached","profiles":["Acme wave app","Acme wave uploadwidget"]}
{"event":"step","step":"build","phase":"start"}
{"event":"step","step":"build","phase":"end","ok":true,"durationMs":72000,"buildNumber":"1786437647","configuration":"Debug"}
{"event":"step","step":"install","phase":"end","ok":true,"durationMs":18000,"device":{"id":"10BFE051","name":"Dev's 17 Pro"},"bundleId":"io.acme.app.dev.wave"}
{"event":"done","ok":true,"durationMs":113000,"devices":1,"bundleId":"io.acme.app.dev.wave"}

Every object has an event. start names the run, devices and signing carry the header facts, each step appears twice (phase start then end, the second with durationMs and ok), and done closes the stream with ok and the total. jetbridge link adds link, with the install, index and manifest URLs. A failure emits error with the message, then done with "ok":false, and the exit code is non-zero — so a reader can loop until done and never hang.

Every command opens with start and closes with done, so a reader can loop until done and never hang. init and reindex carry no header rows and add one event of their own (init, reindex) before closing.

bundles ls --json is the one exception: it prints a single array of variants, the shape it always had, and nothing else — no start, no done. Read it with JSON.parse on the whole stream, not line by line.

Without --json, stdout carries only a command's own payload (the bundles ls table); the progress you read while it runs is stderr either way. On a terminal a step rewrites its own line as it finishes; redirected to a file, each step prints one plain line and no escape codes.

Guides

  • Adopting jetbridge — an existing app from nothing to a working variant install: what init infers, the xcconfig wiring, reduced entitlements, extensions.
  • Credentials — creating the App Store Connect key, where it lives, registering devices.
  • Troubleshooting — organised by the symptom you see, because the visible error rarely names the cause.
  • Releasing — how a change reaches npm.

Requirements

  • macOS with Xcode and its command line tools
  • Node 20 or newer
  • An Apple Developer account, for anything that signs for a device

License

MIT