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

@s2script/sdk

v0.28.0

Published

s2script SDK — the builtin capability types (imported as @s2script/sdk/entity, …) plus the `s2s` CLI for building Source 2 / CS2 plugins.

Readme

@s2script/sdk

The s2script SDK — the TypeScript types and the s2s CLI for building Source 2 / Counter-Strike 2 server plugins.

s2script is a plugin framework for Source 2 games, loaded via Metamod:Source. This package is what you develop against; the runtime ships with the server addon.

Quickstart

npx @s2script/sdk create my-plugin
cd my-plugin
npm run build          # → dist/<id>.s2sp

Copy the .s2sp into addons/s2script/plugins/ on a running server and it loads immediately — re-drop to hot-reload, delete to unload. No restart.

import { command, hook } from "@s2script/sdk";
import type { Client } from "@s2script/sdk";

export function OnPluginStart(): void {
  command("hello", (cmd) => {
    cmd.reply("hello from s2script");
  });
  hook.on("player_spawn", (ev) => {
    console.log("spawned", ev.getPlayerSlot("userid"));
  });
}

export function OnClientPostAdminCheck(client: Client): void {
  client.chat("welcome");
}

For an existing plugin with s2script.gamedata declarations, run npx @s2script/sdk migrate engine-functions [plugin-dir]. The command analyzes the v1 declarations and source references, reports ambiguities, and writes gamedata/functions.jsonc only when the declared contract can be converted without loss. It refuses an existing destination unless you pass --force. Its JSON report lists files actually changed and the package/source edits you must make manually, including removal of the old s2script.gamedata field and authored engine permissions when adopting the v2 file. A successful conversion does not verify that the v1 declaration listed every native argument or that the resulting call works on the current game build. The command alone does not provide the runtime compatibility facades for old source code. V1 entity hooks are refused because their callbacks may receive a null receiver, which the current v2 receiver contract cannot preserve. With --force, the report identifies the exact create-or-replace path; concurrent prior existence cannot be established by the atomic rename.

This package is types-only — the engine injects the implementation at load time, and s2s build marks @s2script/* external rather than bundling it. Capabilities are imported as per-capability subpaths (@s2script/sdk/entity, @s2script/sdk/timers, @s2script/sdk/clients, …); game-specific schema types ship separately in @s2script/cs2.

Not every package is a plugin: npx @s2script/sdk create --library scaffolds a library — build-time TypeScript a plugin bundles into its own .s2sp rather than something the host loads on its own. Add a published one to a plugin with npx @s2script/sdk add <name>, which vendors its code and types rather than adding an npm dependency (see examples/library-package + library-consumer).

Workspaces

A directory whose root package.json carries an s2script.workspace.plugins glob list — next to npm's own workspaces field — is a workspace: a repo holding several plugins that build, publish, and version together.

{
  "name": "my-server-plugins",
  "private": true,
  "workspaces": ["plugins/*", "packages/*"],
  "s2script": { "workspace": { "plugins": ["plugins/*"] } }
}
npx @s2script/sdk create --workspace my-server-plugins
cd my-server-plugins
npx @s2script/sdk create shop      # detects the workspace, writes plugins/shop (no per-plugin devDeps)
npm install                        # links every member into node_modules
npx @s2script/sdk build            # every plugin, dependency order; --filter <pattern> to narrow
npx @s2script/sdk deploy           # build + publish, skipping `private` and already-shipped versions
npx @s2script/sdk version          # applies pending changesets, cascading sibling version ranges

Finding no s2script.workspace marker above the target directory, every command behaves exactly as it does today — workspace mode is opt-in by construction. s2s build <one-plugin-dir> (naming a specific plugin rather than the root) always stays single-plugin mode even inside a workspace; --filter is the workspace-mode way to narrow to a subset, by package name (@me/shop, @me/*) or by path glob (plugins/sh*). --stamp-version <v> (build-only) rewrites every plugin's version and the sibling ranges that would otherwise break.

Sibling interfaces need no .s2script/types/ copy. npm already symlinks every workspace member into node_modules — declared as a dependency or not — so a consumer's import type { ShopAPI } from "@me/shop" resolves straight to @me/shop's own published types file under moduleResolution: "bundler", no config and no copy required. Contrast this with depending on a registry package (s2s add @edge/admin-core): that pulls the producer's .d.ts down into your own .s2script/types/<dep>/index.d.ts, because a registry producer isn't sitting in the same checkout — a byte-copy (hashed into manifest.compiledAgainst, re-verified against the producer's published hash at load) is the only way your build can see its shape at all, and it goes stale the moment the producer's interface changes until you re-run s2s add. A workspace sibling's contract can never go stale that way: the file your build resolves to is the producer's current contract, on the same disk, in the same commit. If a plugin still carries a pre-migration .s2script/types/<dep>/ copy after joining a workspace, the live sibling wins and the build warns that the copy is now dead weight.

private: true means built, never published. Ordinary npm/Changesets semantics, not a new field: s2s build builds every plugin in a workspace regardless of private, but s2s deploy prints a per-plugin plan that skips a private: true package by name instead of shipping it. That's what lets a workspace hold a large first-party or in-house plugin suite that must never reach the public registry — build it, drop the .s2sps into your own server, never s2s deploy it. (s2s deploy <dir> on a single private plugin outside a workspace refuses the same way.)

Bundles

A workspace whose plugins only make sense together (a game mode split into several plugins) can publish as one bundle: the registry lists it as a single entry, installs it with one command, and records which member versions each release pinned. Make the workspace root the bundle:

{
  "name": "@edge/jailbreak",
  "version": "1.0.0",
  "workspaces": ["plugins/*"],
  "s2script": {
    "kind": "bundle",
    "workspace": { "plugins": ["plugins/*"] },
    "bundle": { "optional": ["@edge/jailbreak-gangperks"] }
  }
}
  • Members are the workspace's non-private plugins, and must share the bundle's scope. A bundle has no code of its own, so the root has no main and must not be private.
  • s2s deploy at the root publishes the members first, each claiming the bundle, then the bundle itself, pinning every member to the version just built. The plan shows it as the last row. A bundle version already published with different member versions is refused before anything uploads — bump it.
  • s2s version bumps the bundle whenever a member is released, by the largest member bump.
  • s2s install @edge/jailbreak installs the required members (and whatever they depend on); --with @edge/jailbreak-gangperks adds an optional one. s2s add refuses a bundle: depend on a member's interface instead.
  • A plugin already published before it joined a bundle must be republished at a new version from the bundle's workspace, so that its registry entry carries the claim.

The membership claim travels in the registry upload only — the .s2sp manifest the runtime loads is unchanged, and the runtime has no notion of bundles.

Publishing

s2s deploy builds the package and publishes it to the registry. Its page on s2script.com is filled in from what you already keep beside your code:

  • README: README.md next to package.json (found the way npm finds one, up to 100,000 characters). Each deploy that carries one replaces the page's README.
  • Source link: package.json's standard repository field, in any npm form. { "url": …, "directory": … } links straight into a monorepo directory.
  • Config: the s2script.config block, shown as a table of options with their defaults. Options marked "sensitive": true keep their default off the page.

The README and repository travel with the deploy, not inside the .s2sp: the runtime never sees them. A deploy without them leaves what the page already shows, so a README or link set on the site stays until a deploy replaces it.

Docs

s2script.com/docs — getting started, guides, and the full API reference. Source and issues: GitHub.

License

MIT OR Apache-2.0

Typed plugin interfaces

Opt in with s2script.interfaceProtocol: 2, export a self-contained Contract with methods and forwards: { EventName: Notification<Payload> }, then use publish(name, implementation) and use(name) without caller-selected generics. s2s build generates the name association and checks wire schemas; s2s add obtains only a plugin dependency's types. Include the generated .s2script/interfaces.d.ts in existing projects' tsconfig files. Protocol 2 needs host API 3.

CJS module evaluation runs before the host opens its load window. Register publications, subscriptions, binding maps and optional watches inside OnPluginStart. Exported handler functions and imports can remain at module top level.

Inline handlers infer their payload from the provider:

import { use } from "@s2script/sdk/plugin";
export function OnPluginStart(): void {
  use("@demo/racing").on("OnRunFinished", event => console.log(event.elapsedMs));
}

Use an explicit binding map when a named local export has a different name from the provider forward. The returned subscription disposes the whole map:

import { bindForwards, command } from "@s2script/sdk";
export function OnRaceFinished(event: { elapsedMs: number }): void {
  console.log(event.elapsedMs);
}
export function OnPluginStart(): void {
  const racing = bindForwards("@demo/racing", { OnRunFinished: OnRaceFinished });
  command.server("sm_disable_racing", () => racing.dispose());
}

bindForwards requires a hard plugin dependency. Optional providers continue to attach through watchOptional, where named local functions can be passed to the supplied service.on. See the complete migration guide for supported types, copying, dispatch, and compatibility limits.

Use Hook<Payload> for synchronous decisions and Transform<Payload, WritableKeys> for validated shallow patches. Producers call dispatch; notifications use emit. All crossings copy supported wire values. Listeners must return synchronously, and invalid/throwing listeners contribute Continue. Nested crossings stop at 32 active calls. Retained optional services throw InterfaceUnavailable after detach; reacquire through watchOptional on each provider generation and own resources in its scope. tryUse is a one-time lookup.

For an existing consumer, declare the provider under s2script.pluginDependencies or optionalPluginDependencies, run s2s add <provider>, include .s2script/interfaces.d.ts, and rebuild with s2s build. Acquisition needs only the verified declaration. Rebuild and deploy the producer and consumers together for protocol 2; API 3 hosts still accept legacy API-2/protocol-1 archives, while old API-2 hosts reject new API-3 builds. See the acceptance workspace for executable examples and the separate live verification sequence.