Publishing to the registry
Community plugins ship through the s2script registry on this site: one identity, one version, dual artifacts (.s2sp for operators + types tarball for authors). Publish is open; review is additive — new versions start unreviewed with a public disclaimer.
The CLI is s2s, shipped in @s2script/sdk. Use s2s … when the package is installed, or npx @s2script/sdk … otherwise.
Prerequisites#
- Create an account at /signup
- Claim a scope under Organizations (e.g.
@acme) — one scope per org - Authorize the CLI (CLI login) or mint a deploy token
Unscoped names (rtv) are owned by the first user who deploys them — no org required.
Authenticate#
Local (device login)#
s2s login
# or: npx @s2script/sdk loginThe CLI calls POST /api/v1/auth/device/start, prints a short code, and opens /cli/authorize. Approve in the browser; the CLI’s next POST /api/v1/auth/device/poll returns a s2s_… deploy token, which it stores in ~/.s2script/credentials.json.
Paste path (headless boxes):
s2s login --token s2s_…CI#
env:
S2SCRIPT_TOKEN: ${{ secrets.S2SCRIPT_TOKEN }}
run: npx @s2script/sdk deploy --ci--ci never prompts; missing S2SCRIPT_TOKEN exits non-zero.
package.json#
{
"name": "@edge/heal-zones",
"version": "1.0.0",
"types": "api.d.ts",
"s2script": {
"apiVersion": "1.x",
"publishes": { "@edge/heal-zones": "1.0.0" },
"pluginDependencies": { "@s2script/cs2": "^1.0.0" }
}
}If s2script.publishes is set, "types" must point at an existing .d.ts. The CLI packs that file into a types-only tarball; the registry rejects uploads that omit it (400 publishes_requires_types). Runtime-only plugins omit publishes and skip types.
Deploy#
s2s deploy- Runs the publishes ⇒ types gate
- Builds the
.s2sp - Packs types when needed
- Reads
README.mdbeside package.json, if there is one (SDK 0.28.0 and later) - Packs the package’s source into a tarball, choosing files the way
npm packdoes (SDK 0.29.0 and later; see Source tarball) POST /api/v1/deploywithAuthorization: Bearer s2s_…: oneapplication/octet-streamzip holdingmanifest.json,plugin.s2sp, and optionallytypes.tgz,README.mdandsource.tgz. The manifest also carries package.json’srepositoryfield.
Success returns { name, version, reviewState: "unreviewed" }. Manage versions, yank, descriptions and the README from the publisher dashboard.
Source tarball#
Like npm publish, every s2s deploy (single plugin and workspace) also uploads the package’s source as an npm-shaped tarball: gzip+ustar, every entry under package/, so the same files give the same bytes. The registry stores it as source.tgz with the version. Anyone who can see the package can download it from the package page or /api/v1/download/source?name=<name>&version=<version>.
The package page’s Code tab shows it the way npm’s does: a file tree for any version that has source, with each file highlighted and every line linkable (?tab=code&v=1.2.0&path=src/plugin.ts#L12). Markdown files render as a page. Binary files and files over 1 MB are not shown there, but they are in the download.
File selection follows npm-packlist (npm 10):
- With a
filesarray in package.json, only what it lists is packed — globs,!negation, and directories all work. The package root’s own ignore file is not read (filesreplaces it). Ignore files in subdirectories still apply. - Without
files, everything is packed except what.npmignoreexcludes. With no.npmignore,.gitignoreis used instead. Ignore files apply at every depth. A workspace member also honours ignore files up to the npm workspace root. - Always packed:
package.json, README, COPYING, LICENSE/LICENCE, and the filesmain/binname. - Never packed: VCS directories, the package root’s
node_modules(a nested one is packed, as on npm),.npmrc, the ignore files themselves, root lockfiles, and editor litter. - One deliberate difference from npm:
.envand.env.*are never packed..env.exampleand.env.sampleare.
A tarball over the registry’s 16 MiB cap is refused locally. s2s pack --dry-run lists what is packed; then narrow files or add a .npmignore. Versions deployed before SDK 0.29.0 have no source.
s2s pack#
s2s pack [dir] [--dry-run] [--json] [--pack-destination <dir>]What it lists is exactly what a deploy would send. It prints npm’s Tarball Contents / Details report to stderr and writes <scope>-<name>-<version>.tgz to the current directory (or --pack-destination); only the filename goes to stdout. --dry-run lists without writing. --json prints a JSON array instead of the report.
Package page#
Each package has a public page at /plugins/<name>. Below the description, a plugin’s page has two tabs: README and Config. Link straight to the Config tab with /plugins/<name>?tab=config.
README. Markdown that explains what the plugin does, its commands, permissions and setup. Keep it as README.md next to package.json: since SDK 0.28.0, s2s deploy sends it, and each deploy that carries one replaces the page’s README. You can also write or edit it on the package’s page in the publisher dashboard, which has a preview. A deploy without a README keeps the current one, so an edit made on the site stays until your next deploy that has one.
The README renders like these docs, with tables, highlighted code fences and blockquotes. Raw HTML shows as text, and links must be https:, http:, mailto: or on this site. Images must be https:. The limit is 100,000 characters.
Config. Built from the s2script.config block in your package.json, as published in the newest non-yanked version. You don’t write it separately. Each option gets a row with its key, type, default, range or allowed values, and description. Below the table are the block as declared in package.json and the config file the runtime writes on first load. Options with a group are listed under that heading. A label shows under the key.
Source. A link under the package name to where its code can be browsed. It comes from package.json’s standard repository field, which s2s deploy sends since SDK 0.28.0. Any npm form works, including { "url": …, "directory": … }, which links into a monorepo directory on GitHub, GitLab and Codeberg. You can also set it on the package’s page in the publisher dashboard: an https URL, or owner/repo for GitHub. A deploy without repository keeps the current link.
Restricted packages#
Like npm, a package’s access is public or restricted. A restricted package is visible only to the members of its org, whatever their role. Everyone else gets the same 404 as for a package that doesn’t exist, and it’s left out of the plugin catalog. Members find it on their dashboard.
Publish one restricted with npm’s flag, or set it in package.json:
s2s deploy --access restricted{
"name": "@edge/anticheat",
"publishConfig": { "access": "restricted" }
}- Only a scoped package can be restricted (
400 restricted_requires_scope), because only an org has members. - Access is set when the package is first published. A later deploy that asks for a different access is refused (
409 access_mismatch). A deploy that doesn’t name an access keeps the current one. - A public bundle can’t include a restricted plugin. A restricted bundle can include either.
- Restricted artifacts never go through
packages.s2script.com. Downloads are checked against the token and never cached.
Installing. s2s install and s2s add send your login, so members install restricted packages as usual. A server or CI job that only installs should use a read-only token. Create one at Deploy tokens, optionally bound to the org, and set it as S2SCRIPT_TOKEN. Publish tokens can download too. For the npm-compatible /npm/* surface, give npm the same token with //www.s2script.com/npm/:_authToken=${S2SCRIPT_TOKEN} in .npmrc.
Operator & author consumers#
| Who | Command | Artifact |
|---|---|---|
| Dependent author | s2s add @edge/heal-zones@^1 | types only |
| Server operator | s2s install @edge/heal-zones | .s2sp closure |
Types are also available via the npm-compatible surface at /npm/* (never serves .s2sp). s2s add for a plugin dependency downloads types only (registry metadata + types artifact), not the producer binary — operators still s2s install the .s2sp closure. Protocol 2 producers stamp host API 3.x; see inter-plugin interfaces.
s2s install --disabled puts that closure in plugins/disabled/ and records it under "disabled" in plugins.json until s2s enable. See Install a plugin without loading it.
Yank#
From the dashboard, or:
curl -X POST https://s2script.com/api/v1/yank
-H "Authorization: Bearer $S2SCRIPT_TOKEN"
-H "Content-Type: application/json"
-d '{"name":"@edge/heal-zones","version":"1.0.0"}'Yanked versions are hidden from default resolve; lockfile pins can still warn.
Reserved#
@s2script/* is first-party only. Community tokens cannot deploy into that scope.
Next#
- Authoring plugins — scaffold, build, plugin shape
- Inter-plugin interfaces —
publishes/ producer-as-import - Plugin catalog — public package pages + disclaimer