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

@mutmutco/installer-launcher

v1.3.1

Published

Single-executable (SEA) launcher: sign-in, gated payload fetch, self-update. One copy ships per product.

Readme

launcher — single-executable installer for one product

A small Node >= 22, ESM, TypeScript + vitest single-executable-application (SEA) launcher. One copy ships per product: the binary bakes a product.json asset, signs in the user, fetches the gated release payload, verifies its Ed25519 signature, and unpacks it under the product dir. Zero runtime dependencies — node: builtins only (fetch is global in Node 22). Dev dependencies: typescript, vitest, @types/node, esbuild (mirrors cli/).

Reference: the google loopback PKCE flow is lifted from MM-Strategy's src/cli/login.ts (read-only reference) — same four steps (loopback listener, dynamic client registration, PKCE + browser, code exchange), adapted to the launcher's token store.

Commands

launcher login [--config <path>] [--dir <path>]    sign in (the ONLY command that opens a browser)
launcher logout                                     wipe tokens AND payload
launcher install                                    refresh the launcher, login if no token -> fetch+verify+unpack -> last mile
launcher update [--dry-run]                         refresh the launcher, then the payload; no-op when current
launcher status [--json]                            read-only: launcher, payload vs the release, sign-in, auto-update
launcher doctor                                     launcher report (no network), then the payload's doctor
launcher autoupdate on|off|status                   hourly per-user schedule (hidden via wscript on Windows)
launcher <verb> [args…]                             forwarded untouched when payload.json declares it
launcher [-p …|--model …]                           no verb: starts the product's session; leading flags the launcher does not own pass through
launcher --run <file> [args…]                       run a payload ESM file with the launcher's own runtime
launcher --version                                  `<bin> <payload version> (launcher <version>)`

Every screen that is not an install/update run is drawn by @mutmutco/installer-face (#7072): usage, notices, the status/doctor report, and refusals (stderr). Launcher flags (--config, --dir, --json, --no-color, --dry-run, --help, --version) are read before the command and around the launcher's own verbs only. Only mutating verbs take the installation lock.

@mutmutco/installer-launcher/refresh exports refreshLauncher and readLauncherRecord (#7070): the signed GET <host>/release/launcher record (manifest shape and signing) keeps the one-line installer's launcher current from every update path. A SEA run records its own path in <productDir>/launcher.json so the refresh finds it.

--run <file> [args…] runs a payload file with the launcher's OWN embedded runtime — no Node on the machine. <file> resolves against the cwd and is loaded with import(); the file sees process.argv = [execPath, <abs file>, ...args] and its process.exitCode is propagated. A missing file prints one sentence on stderr and exits 1. --run never loads the product config and never reads the token store, so it works with no config and no login. The file must be self-contained ESM: bundled, with no node_modules resolution across the SEA boundary and no require() of anything that is not a node: builtin.

--config <path> / LAUNCHER_CONFIG selects the product config; --dir <path> / LAUNCHER_DIR overrides the product dir. Flags work before or after the command.

Only login (and the login half of install) opens a browser:

  • github: prints the user_code + verification_uri and opens the browser (verification_uri_complete when the server sends one). Polls the device token endpoint per interval; on slow_down adds 5s. Set LAUNCHER_NO_OPEN=1 to print only (CI/tests).
  • google: loopback 127.0.0.1 redirect with PKCE; the browser opens automatically.

The product dir defaults to ~/.<product>/ on mac/linux and %LOCALAPPDATA%\<product> on Windows, and holds tokens.json (0600), state.json, and payload/.

Config

config/product.template.json per product:

{
  "product": "example-product",
  "host": "https://gate.example.com",
  "loginKind": "github",
  "githubClientId": "EXAMPLE_CLIENT_ID",
  "publicKey": "<base64 of the RAW 32-byte Ed25519 public key>",
  "binName": "example-product"
}

loginKind is "github" or "google"; githubClientId is required for the github kind. publicKey is the base64 of the raw 32-byte Ed25519 key the manifest signature verifies against. No secrets are committed — the client id and public key are public; the client secret (github) lives only on the gate server.

Build

npm install
npm run build            # node build.mjs -> dist/launcher.js + dist/launcher.sea.cjs (+ dist/product.json staging)
npm run typecheck        # tsc --noEmit
npm test                 # vitest run (builds dist/ first for the integration test)
node build.mjs --product-config ./config/my-product.json   # stage a real product asset
node build.mjs --sea     # also stamp the SEA single-executable (needs postject)

Two bundles ship from one source: dist/launcher.js is ESM (run by node in dev and by the integration tests) and dist/launcher.sea.cjs is CommonJS, baked as the SEA main (see below).

How it meets the contract (wire contract v1)

  • Device flow (github): POST {host}/gate/device/code body {client_id} -> 200 {device_code, user_code, verification_uri, verification_uri_complete?, expires_in, interval}; POST {host}/gate/device/token body {client_id, device_code} -> 200 {access_token, refresh_token, expires_in} or 400 {error: authorization_pending|slow_down|expired_token|denied}. Implemented byte-identically in src/login-github.ts.
  • Refresh: POST {host}/gate/refresh body {refresh_token} -> 200 {access_token, expires_in} or 403 {error}. Every start refreshes the access token before expiry (within 60s); token TTL is <= 3600s.
  • Gated reads: Authorization: Bearer <access_token> on GET {host}/release/manifest (200 {version, created, files: [{path, sha256, size}], signature}) and GET {host}/release/<path> (raw bytes, 404 {error} when absent).
  • Manifest signature: base64 detached Ed25519 over exactly the UTF-8 bytes of canonical JSON {"created":…,"files":[{"path":…,"sha256":…,"size":…}],"version":…} (keys sorted, no whitespace; created used raw, never reformatted). Verified with the baked public key BEFORE writing anything, through the ported src/canonical.ts (verbatim from installer/gate/src/canonical.ts: UTF-16 code-unit key sort, arrays keep order, undefined members dropped, non-finite numbers throw, strings JSON.stringify-escaped).
  • Refusals: 401 {error:"unauthorized"} -> re-login (stored refresh is retried first, then a fresh login); 403 {error:"forbidden"} -> revoked/allowlist-miss, stops with the plain sentence "this install is not allowed for your account — access was revoked or never granted." The access token is opaque (server-side HMAC) — stored and relayed, never parsed.
  • google loginKind: no /gate/device/* endpoints; the loopback PKCE flow (src/login-google.ts) yields the bearer directly, then the same /release reads apply.
  • Behaviors: install = login if no token -> fetch+verify+unpack under the product dir (download to a temp staging dir, verify every size + SHA-256, then swap into payload/) -> print next step; update = same fetch, re-checks manifest; doctor = config, token presence/expiry, payload version, path wiring; logout = wipe tokens AND payload.

The last mile (payload.json.entry) and $self

install ends by reading the payload's payload.json entry and running it with cwd on the payload dir (relative paths, no home-dir quoting hazard). entry is either a whitespace-separated string or a string array. Its first element may be the token $self, replaced at run time by process.execPath — the launcher binary itself. A product that must not require Node on the machine therefore declares:

{ "entry": ["$self", "--run", "dist/index.mjs", "install", "--from-payload"] }

the launcher's embedded runtime then runs the payload's bundled ESM. Everything else in defaultRunEntry is unchanged: when the command cannot run (spawn error, non-zero exit) the launcher prints the exact command instead and still exits 0 (the command shown is the $self entry already resolved).

SEA: import() and the embedded runtime

launcher --run relies on Node's dynamic import() of an on-disk ESM file inside the SEA binary. Proven locally on Node v24.20.0 with postject:

  • A Node 22/24 SEA main script is always CommonJS: mainFormat exists only in Node >= 25.5, and injecting the ESM dist/launcher.js as the main fails with SyntaxError: Cannot use import statement outside a module. build.mjs therefore also bundles a CommonJS dist/launcher.sea.cjs (target esnext) and bakes THAT as the SEA main; src/module-url.ts bridges __filename (CJS) and import.meta.url (ESM).
  • import() of an on-disk .mjs from a CommonJS SEA main works: the probe imported the file, the file saw argv = [execPath, file, ...args], and the process exited with the file's code.
  • Node documents "import() does not work when useCodeCache is true", so the generated sea config sets useCodeCache: false. (On 24.20 the probe also passed with it true; the flag stays false so the feature never leans on that caveat.)
  • The postject sentinel fuse is a hash embedded in the Node binary that changes between releases (24.20 carries NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2; older docs cited NODE_SEA_FUSE_fce680ab2cc467b6e840016ee2343c1a). build.mjs reads it out of process.execPath instead of hardcoding it.
  • disableExperimentalSEAWarning stays true.

The reusable workflow .github/workflows/launcher-build.yml smoke-tests this end to end: after --version it runs --run test/fixtures/run-hello.mjs a b against the stamped binary and asserts the fixture's stdout.

Tests

  • Unit: test/canonical.test.ts (canonicalization + Ed25519 verify with a generateKeyPairSync('ed25519') keypair), test/flows.test.ts (device-flow polling/backoff, refresh, unpack to temp dir, refusal paths), test/store-google.test.ts (token store + google loopback PKCE against a fake OAuth server), test/last-mile.test.ts (the payload.json entry, the $self token, the manual-command fallback, and --run argv/exit-code/missing-file).
  • Integration: test/integration.test.ts runs the built dist/launcher.js through a full install -> doctor -> update -> logout cycle against a localhost fake gate + release server (device polling, refresh, signature verification, 403 revocation, payload swap) and spawns --run test/fixtures/run-hello.mjs a b for a real stdout + exit-code assertion.

Release CI (SEA binary)

node build.mjs --sea follows the Node SEA docs exactly:

  1. esbuild bundles dist/launcher.js (ESM, for node) and dist/launcher.sea.cjs (CommonJS, the SEA main — Node 22/24 SEA main scripts are CommonJS-only);
  2. a dist/sea-config.generated.json is written with the baked product.json asset, useCodeCache: false (the import() last mile), and main = dist/launcher.sea.cjs;
  3. node --experimental-sea-config <generated> produces dist/sea-prep.blob;
  4. process.execPath is copied to dist/<platform>-<arch> and stamped with postject <binary> NODE_SEA_BLOB <blob> --sentinel-fuse <fuse read from the Node binary>. The name is the EXACT download name the bootstrap scripts request (/dl/<product>/<platform>-<arch>, platform darwin|win, arch arm64|x64; win32 maps to win, and there is no .exe suffix — install.ps1 downloads win-x64 and renames it to <product>.exe).

The SEA binary must be stamped on a native runner for its target OS/architecture — the Node SEA blob embeds the host Node binary, so a macOS arm64 binary cannot be produced on Linux. The reusable workflow .github/workflows/launcher-build.yml implements exactly the steps above on the two supported lanes (macos-14 arm64, Apple Silicon, and windows-latest x64 — Intel Macs are not supported, so there is no darwin-x64 leg): npm ci, install postject, node build.mjs --product-config <cfg> --sea, smoke --version then --run test/fixtures/run-hello.mjs a b, write the .sha256, and upload launcher-<product>-<platform>-<arch>. It is callable (workflow_call) and dispatchable, and it is the only supported path to a real SEA binary — nothing is faked in this lane.

Signed fresh acquisition

A signed payload.json can declare acquisition instead of implementing installation:

{
  "acquisition": {
    "schema": 1,
    "package": "@scope/product",
    "archive": "product-1.2.3.tgz",
    "platforms": ["win32-x64", "win32-arm64", "darwin-arm64"],
    "convergeEntry": "dist/index.js",
    "convergeArgs": ["install", "--from-shared-prefix", "$prefix", "--version", "$version"],
    "rollbackEntry": "dist/index.js",
    "rollbackArgs": ["install", "--rollback-shared-prefix", "$prefix", "--version", "$version"],
    "runEntry": "dist/launcher-entry.js",
    "repairArgs": ["--install-converge"]
  },
  "entry": ["$self", "--run", "acquisition-required.mjs"],
  "verbs": ["start", "doctor"]
}

The archive and metadata must both be covered by the release signature. Entry paths are package-relative; placeholders replace entire argv elements only. The launcher owns a pinned, checksum-verified user-local Node 24.20.0/npm 12.0.2, local-archive project-prefix installation with lifecycle scripts disabled, credential-free public npm resolution, and stable candidate paths. Existing system Node/npm installations are unchanged. Updates acquire the selected archive even when a previous product root exists. An unchanged update reuses its candidate and runs runEntry with repairArgs for idempotent selected-product convergence; it never repeats acquisition activation or reports ready from directory existence alone. Products own validation of their complete release and their activation/host convergence through the declared commands.

Convergence runs the candidate entry directly with real Node, never SEA re-entry. Forwarding also uses real Node so product grandchildren can use process.execPath. The consumer rollback command must durably record prior/candidate selection before activation, compare-and-restore under its own lock, restore absence for a fresh failed install, and succeed harmlessly if no activation occurred. It must refuse to overwrite a newer concurrent product selection.

A flushed shared pending receipt precedes convergence. The launcher commits its selection atomically only after convergence succeeds. On failure or interrupted startup it invokes the consumer rollback before allowing another install or forwarding. Failed rollback stays pending. A crash after shared commit completes the receipt instead of reverting the committed product. Host registration is not atomic: failures can leave host references to retained candidates. After a committed promotion the launcher removes candidate folders state.json no longer references (the active one is kept; symlinks and junctions are skipped; a failed delete is logged). A failed activation keeps its candidate. An orphan lock-recovery guard fails closed and needs operator diagnosis.

For already-shipped old launchers, copy the exact file exported by @mutmutco/installer-launcher/acquisition-required into the signed payload as acquisition-required.mjs, and declare the legacy entry above. Old launchers truthfully refuse and instruct the user to rerun the public installer. New launchers use acquisition instead.