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

@xeonr/containers

v0.5.0

Published

Client SDK for the xeonr container-platform — jobs, sandboxes, workspaces, environments and apps over ConnectRPC.

Readme

@xeonr/containers

TypeScript client for the container-platform — the sandbox runtime service at containers.xeonr.io. Jobs, environments, workspaces, sandboxes and apps, without writing the same 500 lines of ConnectRPC glue every consumer has written so far.

Design and rationale: container-platform/docs/SDK-DESIGN.md (internal).

pnpm add @xeonr/containers @bufbuild/protobuf @connectrpc/connect

Ships ESM and CommonJS. A CommonJS project imports it the same way and needs no tsconfig change — require() of an ESM-only package needs TypeScript 5.8+, and charging a consumer a module-system migration for adopting a client library is not a trade worth making. The peer dependencies stay external to both halves, so there is only ever one protobuf-es schema registry in a process.

import { createClient, egress } from "@xeonr/containers";

const cp = createClient({
  endpoint: "https://containers.xeonr.io",
  apiKey: process.env.CP_API_KEY,   // namespace-scoped key (cpk_…)
  namespace: "amelia",
});

const envId = await cp.environments.ensure(
  "reg.xeonr.io/xeonr/runner-node@sha256:…",
);

const run = await cp.jobs.run({
  environment: envId,
  argv: ["node", "build.js"],
  resources: { cpu: "500m", memory: "256Mi" },
  timeout: "10m",
  egress: egress.presets("npm"),
});

if (!run.ok) console.error(run.terminationName, run.exitCode, run.logs);

What it gives you

The transport, correct. Always HTTP/2 in Node — over an http:// in-cluster URL that is h2c, which is what the platform serves. There is no knob, because every other value is wrong: client and bidi streaming over HTTP/1.1 deadlock with no error on either side.

Errors that split the two kinds of failure. Transport and control-plane failures throw (NotFoundError, PermissionDeniedError, QuotaExceededError, …). A workload that exits non-zero is a value — run.ok === false — because the RPC succeeded and told you the exit code. run.check() escalates it if you want the exception.

Cancellation that cancels. The api runs jobs under context.WithoutCancel, so dropping a RunJob stream leaves the pod running. Pass an AbortSignal and the SDK issues CancelJob before letting go. { onAbort: "detach" } opts out.

environments.ensure(). RegisterStaticEnvironment always creates — by design — so resolving a digest to an environment id needs a cache, a look-before- register, and single-flight or a deploy mints duplicates. That is this call.

Human units in, protocol units out. "10m", "500m", "256Mi" — and unparseable input throws rather than becoming 0, which the platform reads as "no limit".

Streams drained once. run() returns a typed outcome and takes onLog / onState / onAccepted callbacks. Log text is decoded across chunk boundaries, so multi-byte characters survive; a stream that closes without its terminal event is a named error carrying the job id, not a silent success.

Escape hatch. cp.raw.<service> is the generated client for every RPC, modelled or not; @xeonr/containers/proto is the schema it was generated from.

Status

| | | |---|---| | L1 core — transport, auth, namespace, errors, retry, pagination, cancellation | ✅ | | cp.jobs — run, stream, get, list, cancel, tailLogs | ✅ | | cp.environments — ensure, register, build, get, list, pin, delete | ✅ | | cp.workspaces — files, snapshots, zip import/export, presigned upload | ✅ | | Multi-mount: mounts on sandboxes, jobs and app versions; sandbox.mount() | ✅ | | Ephemeral scratch, seeded forks, workspaces.snapshots() across a namespace | ✅ | | workingDir on sandboxes, jobs and app versions | ✅ | | cp.sandboxes — exec, files, ports, egress grants, sync, lifecycle, heartbeat | ✅ | | sandbox.processes — persistent console sessions | ✅ | | ./recipes — ensureSandbox, agent toolkit | ✅ | | ./testing — fakePlatform() | ✅ | | cp.apps — versions, promotion, rollback, activation, logs | ✅ | | Browser entrypoint — connect-web, cookie auth, bidi refused with a pointer to the WS bridge | ✅ |

The one thing deliberately not built is an apps.deploy() recipe — ensure-App-by-key, create a version, promote. Its shape is recorded in the design doc (§6.6) because it was expensive to learn, but the only consumer that deploys Apps today is functions, which is Go. Build it when something in TypeScript needs it.

Sandboxes, briefly

const sandbox = await cp.sandboxes.create({
  environment: envId,
  mounts: [
    { workspace: ws.id },                       // durable, read-write, outlives the sandbox
    { ephemeral: true, at: "/scratch" },        // created and deleted with the sandbox
    { snapshot: snapId, at: "/ref" },           // immutable, read-only, safe to share
    { cache: "npm", sizeMb: 4096, at: "/cache" },  // a real disk, kept across sandboxes, evictable
  ],
});

// Silence, not wall-clock, is what says a process is stuck. Partial output is kept.
const out = await sandbox.exec(["npm", "ci"], { idleTimeout: "2m" });

// The grant closes with the scope — no id to remember, no window left open.
{
  await using grant = await sandbox.grantEgress(egress.presets("npm"), { reason: "install" });
  await sandbox.shell("npm ci");
}

// A dev server that outlives the call, checked for an instant crash before it returns.
const dev = await sandbox.processes.start("dev", "npm run dev", { cwd: "/workspace" });
if (dev.stateName === "EXITED") console.error(dev.startupOutput);
console.log((await sandbox.expose(5173, { auth: "public" })).url);

Filesystems

A sandbox carries a list of mounts, not one workspace. sandbox.files addresses the primary mount — the first writable one, where the workload's working directory is — so single-mount code reads exactly as it always did. sandbox.mount(pathOrId) reaches a specific one, by its path in the pod or by the id of the tree behind it. A snapshot mount comes back read-only in the type: a snapshot cannot be written by anyone, so the write half is absent rather than present-and-failing.

An ephemeral mount is scratch that dies with the sandbox — but it is a real workspace while it lives, so the whole file API works on it and it can be snapshotted. That is the only way to keep anything in it: snapshot before you delete, because nothing else survives the owner.

There is no implicit /work any more. A sandbox that declares no mounts starts in its image's own working directory with only /tmp writable, which on a read-only rootfs is a terminal you cannot work in — [{ ephemeral: true }] is the usual intent.

Disk-backed mounts

scratch, cache and volume are backed by a disk image rather than by the content store, and they exist for the trees the store is the wrong shape for. The store syncs file by file, so staging cost scales with file count rather than bytes — which is why a node_modules was slow enough to be excluded from sync entirely, and so vanished on every restart. A disk is one image, so the count stops mattering.

{ scratch: true, sizeMb: 8192, at: "/work" }      // dies with the pod; no sync, no bookkeeping
{ cache: "npm", sizeMb: 4096, at: "/cache" }      // same key, same disk, next time — but evictable
{ volume: vol.id, at: "/workspace" }              // durable, archived on a schedule

They are not workspaces, and kindOf reports them as themselves for that reason: nothing syncs them to the store, so the workspace file API does not address them and treeIdOf is empty. Reach their files through the volume file API.

A volume mount takes no sizeMb — the size belongs to the volume, so set it at creation and change it with resize. The server refuses one here rather than honouring it, so that a mount cannot become the authority on a property the volume owns.

sizeMb is a hard cap enforced by the filesystem inside the image — a write past it fails with ENOSPC. That is the point of it: an ephemeral mount is charged to the container's ephemeral-storage limit, where an overrun evicts the pod with no error attributable to the write. The image is sparse, so the cap is a ceiling rather than an allocation and costs nothing until it is used.

Only a volume takes from. Seeding a scratch or a cache is refused rather than ignored: one is destroyed with the pod and the other may be evicted, so the seed would be thrown away and the caller would be left believing their content was in a disk that is empty.

workingDir overrides where the workload starts. Unset, a sandbox or job lands in its first writable mount and an app gets its image's own WORKDIR — an app's image is built elsewhere and that WORKDIR is part of its contract, so a cache or scratch mount must not silently move it. Set it for the case no default can express: a monorepo mounted at /workspace whose build runs in /workspace/apps/web. sandbox.workingDir reads back resolved, so it answers where a terminal will actually open rather than echoing the field.

Reads hit the durable store. A mount is a materialised copy synced back on a ~10s tick: a file that has never reached the store is flushed on demand by the api, but a file changed inside the sandbox is stale until the tick — pass { fresh: true } to force a reconcile first. It costs a whole-tree walk and hash, which is why it is opt-in. sandbox.sync() flushes every writable mount; sandbox.sync({ workspace }) narrows it to one.

Apps, briefly

An App and a version have different lifetimes. The App owns the hostname and the key and lives as long as the thing it serves; a version is an immutable (image, port, filesystems) triple beneath it. Promotion is a pointer flip, which is what makes rollback free — and what makes creating an App per deploy rather than per thing the mistake that churns the URL on every push.

const app = await cp.apps.get(appId);
await app.createVersion({
  environment: envId, port: 8080, promote: true,
  mounts: [{ snapshot, at: "/site" }],
});
await app.rollback();                       // promote the version that was active before
await app.update({ env: { KEY: "v" } });    // mask derived from the keys you pass

An ephemeral mount on a version is per-replica scratch owned by the pod, not shared state: an AppVersion is immutable and shared by every replica, and N replicas writing one workspace is the race the single-writer rule exists to prevent. It dies on every roll, scale-down, restart and reschedule.

update() sends only the fields present on the patch. A full message would re-apply access and autoStart on every deploy, silently re-widening an App that was deliberately locked down.

Recipes

@xeonr/containers/recipes is the opinionated layer — separately imported, built only out of the core, and free to change its mind faster than the core does.

import { ensureSandbox, createAgentTools, memoryStore } from "@xeonr/containers/recipes";

// "a sandbox for this key, usable right now" — creates, resumes, or recreates
// against the RETAINED workspaces, and re-exposes the ports on every branch.
const { sandbox, workspaces, previewUrls, status } = await ensureSandbox(cp, guildId, {
  store,                    // two methods: get(key) / set(key, value)
  environment: envId,
  mounts: [
    { retain: "src" },                    // created once, reused forever after
    { retain: "home", at: "/home/dev" },  // a second slot, its own lifetime
    { ephemeral: true, at: "/cache" },    // fresh on every recreate, by design
  ],
  ports: [5173],
});

// The file/command/process tools an agent needs, as provider-agnostic descriptors.
const tools = createAgentTools(cp, { scope: guildId, store, environment: envId });

A retain slot creates a durable workspace on first use and reuses it forever after — including across a sandbox that had to be recreated, which is the whole point of the recipe. Slots are named rather than counted because source, $HOME and a package cache want different lifetimes and ignore rules, and nesting them under one mount forces one policy on all three. workspaces maps slot name → workspace id.

A sandbox can be suspended, recreated or lost; from where a model sits, none of that ever happens. Every tool ensures first, and a failure comes back as text it can act on rather than as a throw that ends the turn.

Testing

import { fakePlatform } from "@xeonr/containers/testing";

const { client, execScript, sandboxes } = fakePlatform();
execScript.stdout = "ok";

Not a stub library — a real in-memory api the real client speaks the real protocol to, so framing, streaming and error mapping are exercised on the way through. It has already caught one bug the SDK's own tests could not.

Developing

pnpm install
pnpm generate    # regenerate src/proto from the BSR; commit the result
pnpm test        # node --test, no test framework
pnpm typecheck
pnpm build

The generated code is committed and published inside this package. Every TS consumer used to run its own buf generate and commit ~8k lines of output — three copies, three regeneration rituals, three chances to be pinned to a different schema revision than the server. Here the package version pins one revision, and cp.raw is that revision's client.

Source imports carry .ts extensions so Node can run this tree directly with no test dependencies; tsc rewrites them to .js on emit.

build emits ESM via tsc (dist/esm) and CommonJS via esbuild (dist/cjs), then runs scripts/check-dist.mjs — which loads both halves, asserts they export the same names, and fails if a peer was bundled rather than required. The unit tests run against source and can see none of that.