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

@heyocomputer/hws

v0.4.0

Published

The app-lb SDK for TypeScript — deployments, autoscaling, exec and shells, rollouts, namespace plugins and telemetry for heyvm microVMs. Twin of the hws crate.

Downloads

409

Readme

@heyocomputer/hws

The TypeScript twin of hws, the app-lb SDK: register deployments, scale pools, run commands inside a microVM, attach an interactive shell, roll out new specs, and — through a namespace's installed plugins — read its telemetry.

Same wire contract and the same version as the Rust crate (source in this repo, published as hws on crates.io), which also ships the heyctl CLI. Both are checked against the golden fixtures app-lb's own tests write.

npm install @heyocomputer/hws
import { Hws } from "@heyocomputer/hws";

const lb = new Hws({
  server: "127.0.0.1:9090",
  token: process.env.APP_LB_TOKEN,
});

const { stdout, exit_code } = await lb.exec("sb-7f3a9c", "uname -a");

Node 18+, Bun, Deno and browsers. Everything uses fetch; shells use ws on the server and the native WebSocket in a browser.

Authenticating

Prefer an app-token: scoped to particular deployments, revocable without restarting app-lb, optionally expiring. Basic auth also works and is unscoped — it is the operator credential, and the one that mints tokens.

const admin = new Hws({ server, user: "admin", password: process.env.PW! });

const minted = await admin.mintToken({
  name: "agent-runner",
  admin: "admin",
  deployments: ["sb-7f3a9c"],
  expiresInSecs: 86_400,
});

// The secret is in the reply and nowhere else, ever — app-lb stores only its
// hash and no endpoint reads it back.
console.log(minted.token);

A token scoped to specific deployments is refused the fleet-wide routes — listing every deployment, the secret store, and minting — so it cannot widen itself. /metrics is the exception: rather than refusing a scoped token it narrows the answer to what that token can see.

Sandboxes

await lb.createDeployment({
  id: "sb-7f3a9c",
  routes: [],                       // unrouted: reached by exec and shell only
  vm: { driver: "firecracker", port: 8080, disk_size_gb: 20 },
  scaling: { min_replicas: 0, max_replicas: 1, idle_action: "retain" },
});

await lb.waitForReady("sb-7f3a9c", {
  onProgress: (p) => console.log(`${p.healthy}/${p.desired} healthy, ${p.pending} booting`),
});

waitForReady counts healthy backends, not ready — ready is the size of the pool, including a VM that is failing its health check, so waiting on it reports success for a deployment that cannot serve a request.

Shells

const shell = await lb.shell("sb-7f3a9c", { cols: 120, rows: 40 });
shell.onData((bytes) => process.stdout.write(bytes));

process.stdin.on("data", (d) => shell.write(d));
process.stdout.on("resize", () =>
  shell.resize(process.stdout.columns, process.stdout.rows));

const { code, clean, error } = await shell.exit;

Check clean, not code === 0. app-lb reports an unknown exit code as 0, which is what a VM dying under a live session looks like — so a crash and a logout are the same number. clean is false when an error preceded the exit.

There is no resume. If the socket drops the session is gone; reconnecting gives a new shell. This package will not silently retry, because a retry that quietly discards a session is worse than an error.

In a browser

A browser's WebSocket constructor cannot set headers, so a browser shell needs an app-token, which travels in the query string. app-lb accepts ?app_token= on the shell route and nowhere else, for exactly that reason.

A credential in a URL lands in access logs, proxy logs and browser history, so mint a short-lived one:

const ticket = await admin.mintToken({
  name: `terminal ${user}`,
  admin: "admin",
  deployments: [sandboxId],
  expiresInSecs: 120,
});
// hand `ticket.token` to the page, which opens the shell with it

Basic credentials cannot be used for a browser shell at all, and this package throws rather than silently failing the upgrade.

Errors

Every failure is a subclass of HeyctlError carrying status, retryable and isAuth:

| | | |---|---| | UnauthorizedError | 401 — missing, wrong, revoked or expired. app-lb does not distinguish those, so token ids cannot be enumerated. | | ForbiddenError | 403 — the credential was good, the scope was not. Re-presenting it will not help. | | NotFoundError | 404, carrying kind and the name | | NoRunningVmError | 409 from exec/shell with wake: false | | ConflictError | 409 — a job is already running, a secret is still referenced, a rollout's revision moved. code carries app-lb's reason when it gives one, e.g. plugin_not_installed / plugin_disabled | | ColdStartTimeoutError | 503 — no VM appeared in time. retryable. | | UpstreamError | 502 — the daemon failed. retryable. | | MalformedResponseError | a response that could not be interpreted |

try {
  await lb.exec("sb-1", "make test", { wake: false });
} catch (e) {
  if (e instanceof NoRunningVmError) {
    await lb.exec("sb-1", "make test");   // wake it after all
  } else if (e.retryable) {
    // ColdStartTimeout, Upstream, Transport
  } else throw e;
}

Note app-lb answers a failed request in two shapes: {"error": "…"} from its own handlers, and plain text for the 401 and every framework-level rejection (415 for a missing content-type, 422 for well-formed JSON of the wrong shape). This package parses both, so a caller never sees a JSON syntax error where an HTTP status was the actual answer.

Three things this package does not smooth over

A non-zero exit resolves. exec rejects only when the command could not be run. Check exit_code.

exec's timeout does not kill anything. timeoutSecs bounds app-lb's call to the daemon; when it expires you get an UpstreamError and the command keeps running in the guest. The daemon offers no streaming and no cancellation, so output is buffered until it exits. The client-side deadline is sized to outlast the server's worst case — the command timeout plus a possible cold start — because abandoning a request app-lb is still serving turns a well-defined answer into an unexplained transport error.

Registering a deployment does not mean it has a certificate. ACME issuance is asynchronous; poll certs().

Rollouts

replaceDeployment recycles the pool in place. To roll a new spec beside the old pool, verify it, and only then drain the old one:

const { rollout_revision, spec } = await lb.deployment("api");
spec.vm!.image = "api-v2";
const op = await lb.startRollout("api", {
  operationId: crypto.randomUUID(),     // retry a lost reply with the same id
  expectedRevision: rollout_revision!,  // 409 if anything changed it since
  spec,
});
let now = op;
while (now.status === "running") now = await lb.rollout("api", op.operation_id);

Namespace plugins and telemetry

Some plugins install per namespace. The operator enables one for the fleet; a namespace administrator installs it. Installing obs makes app-obs collect every deployment in the namespace, readable with the namespace's own token:

await lb.installPlugin("team-a", "obs");
const obs = lb.obs("team-a");
const fleet = await obs.fleet({ window: "1h" });
let page = await obs.logs("web", { level: "error", limit: 100 });
while (page.next_before_ms !== null) {
  page = await obs.logs("web", { level: "error", limit: 100, before: page.next_before_ms });
}

Where the operator has enabled the postgres plugin, a namespace makes its own databases. Each gets its own role and password, also written to the namespace secret pg-<name> for a deployment's env_from:

await lb.installPlugin("team-a", "postgres");
const pg = lb.postgres("team-a");
const db = await pg.createDatabase({ name: "app" }); // { secret: false } skips pg-app
// db.password and db.url come back this once.
const { databases } = await pg.databases();
await pg.deleteDatabase("app"); // revokes the credential; data and secret stay

The artifacts plugin is the namespace's corner of the global artifact store (where artifact images come from), walled to <namespace>/:

const store = lb.artifacts("team-a");
const v2 = await store.tag("team-a/web:v2");
await store.setTag("team-a/web:latest", v2.digest);   // promote
await store.setRepo("team-a/web", { public: true });  // publish on the hub

Once an auth plugin has declared a provider in the namespace (auth-heyo does by default; auth-google with the namespace's OAuth client), a program puts a deployment behind it:

await lb.setAuth("reports", { provider: "heyo", publicPaths: [{ path: "/healthz" }] });
await lb.clearAuth("reports");   // serves everyone again

Before a plugin is installed every obs, postgres or artifacts call rejects with a ConflictError whose code is plugin_not_installed (or plugin_disabled while the operator has it off).

whoami() says what the server makes of the credential: its tier, whether it is confined, and to which namespace.

Building deployments

Writes take the spec as an object and send it verbatim. Read with deployment(), edit status.spec, and pass that back — PUT replaces the whole spec, so anything dropped in between is genuinely dropped.

const { spec } = await lb.deployment("api");
spec.vm!.image = "api-v2";
await lb.replaceDeployment("api", spec);

Development

npm install
npm test          # builds, then unit tests + the wire-contract check

The wire-contract test reads testdata/wire/*.json — fixtures written by app-lb's own response types — and asserts every key in them is declared in src/types.ts. To a JS client an unknown field and an absent one look identical, which is exactly how five fields once went missing from the Rust client without a test failing. A field app-lb starts sending now fails this test instead of going silently unread.

examples/e2e.mjs runs the whole surface against a live app-lb.

Heyctl is still exported as an alias of Hws, so code written against the pre-0.2 package name keeps compiling after changing its import.