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

@tsg-dsp/cli

v0.20.1

Published

TSG deployment CLI: renders Kubernetes manifests and applies them

Readme

TSG CLI

Renders a TSG deployment to Kubernetes manifests and applies them.

Prerequisites are Node.js 22 and kubectl. The CLI does not use Helm.

Before applying anything, tsg deploy checks the requirements of the rendered bundle. Required CRDs must exist and have a supported version. An older controller version produces a warning but does not block the deployment. CloudNativePG 1.30 or newer supplies the DatabaseRole CRD. The static version table lives in lib/cluster/prerequisites.ts, so deployment does not require an upstream release feed.

deployment.yaml
    ↓  validate → one complete in-memory deployment model
typed Kubernetes resources
    ↓  ordered patches, extra manifests, deletions
final deterministic manifests
    ├── commit to Git → Argo CD / Flux owns apply and prune
    └── tsg deploy → kubectl diff/apply/wait + inventory-backed prune

Commands

| Command | What it does | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | tsg render <config> [-s <id>...] [--include-infra] [-o <dir>] | Writes a manifest bundle without contacting a cluster or the network. | | tsg diff <config> [-s <id>...] | Renders to a temporary bundle and shows what applying it would change, including what would be pruned. | | tsg apply <bundle-dir> | Applies an already-rendered bundle. Never reinterprets the configuration file. | | tsg deploy <config> [-s <id>...] | render → diff → apply → wait → prune over a temporary bundle. | | tsg uninstall <config> [-s <id>...] [--delete-data] | Deletes from the recorded inventory. Data is retained unless --delete-data. | | tsg status <config> | Lists the inventory sets recorded in the cluster. | | tsg migrate <config> [-w] | Rewrites a configuration file without contacting a cluster or the network. | | tsg secrets generate <config> [--apply] | Demo helper for the Secrets that rendered output only references. | | tsg keys generate <stack> <client> | Writes a private_key_jwt pair. Commit the public JWK, not the private JWK. |

Only apply, deploy, diff, uninstall, status, and secrets generate --apply contact a cluster. render and migrate also skip the CLI release check.

Examples

examples/ holds six configurations ordered by how much of the system they use, from one organisation joining an existing dataspace to a full composition cookbook. See examples/README.md; every one of them renders as-is, and test/examples.test.ts keeps it that way.

Configuration versioning

The CLI versions three formats independently. This lets the input schema change without invalidating every bundle, and lets the bundle format change without invalidating cluster inventories.

| Format | Lives in | Marker | | ------------- | ------------------------- | ------------------------------------ | | Input config | the user's Git repository | apiVersion | | bundle.json | rendered output | formatVersion (utils/formats.ts) | | Inventory | the cluster | formatVersion (utils/formats.ts) |

The inventory is the long-lived format. A CLI may read a ConfigMap written months earlier by another version. It rejects an inventory format newer than it understands because a wrong interpretation could prune the wrong objects.

Input versions

tsg --version lists every apiVersion this CLI accepts. Each lives in its own frozen directory under lib/config/ and converts only to its immediate successor. Adding a version therefore needs one converter. Only the newest version has a resolver, so model/ does not handle older versions.

lib/config/
  v1alpha1/schema.ts   frozen at release, never edited again
  current.ts           the newest served version
  versions.ts          the served list, oldest first
  load.ts              validate at the declared version, then convert up

Adding v1alpha2 needs a new directory, v1alpha1/up.ts, one line in versions.ts, one line in current.ts, and a frozen fixture under test/fixtures/versions/. Released schemas are not edited. A file that validated yesterday must validate the same way tomorrow. Tests over the frozen fixtures enforce that rule.

tsg migrate <config> rewrites a file at the newest version, printing to stdout unless -w is given. Comments and key order are not preserved, so review the diff.

When to bump

| Change | Bump? | | -------------------------------------------- | ----- | | New optional field | no | | Removed, renamed, or newly required field | yes | | Changed default that changes rendered output | yes | | Changed semantics of an existing field | yes |

A changed default that produces a different manifest is a breaking change even when existing configuration files still parse.

Unknown fields are hard errors. See forbidNonWhitelisted in utils/validation.ts. Without that check, a renamed field could be ignored and produce the wrong manifests without an error.

The bundle

out/
  bundle.json                 # CLI metadata; never applied as a Kubernetes object
  manifests/
    infra/manifests.yaml
    stacks/
      authority/manifests.yaml
      alfa/manifests.yaml

bundle.json records the resolved image references, CLI version, and input apiVersion. Its layout has a separate formatVersion. Rendering with another CLI version may select different image tags. Set spec.images.<component>.digest for committed bundles.

The stack is the unit of deployment and Kubernetes ownership. Shared infrastructure, the Namespace and in shared database mode the CNPG Cluster, renders separately. A participant is seed data inside a stack and never gets an apply set or inventory of its own.

Topology and participant seeding

The configuration has four distinct concepts:

  • spec.dataspace is the trust domain the stacks in this file join. It names the dataspace and states the authority's DID explicitly, because one dataspace is normally deployed from several files by several organisations and the issuer is usually not one of the stacks in front of you.
  • TsgDeployment is the complete desired deployment described by this file.
  • spec.stacks[] are deployable TSG runtimes. A stack owns its wallet, SSO bridge, optional control/data planes, database resources, patches and apply inventory.
  • stack.participantSeeds[] are dataspace identities to onboard into that runtime. They own no Kubernetes resources.

Every participant is a tenant, even when a stack has only one. Its slug is its ID and its DID is always did:web:<host>:tenants:<id>. This keeps the DID stable when another participant is added later. A wallet-only stack may also start without seeds and onboard participants later.

When a file includes the issuer, dataspace.authority.did must match the DID served by that issuer. The CLI rejects a mismatch.

The renderer writes these records to the wallet's and control plane's initTenants startup configuration. Each control-plane tenant gets its own IAM identity, wallet tenant binding, catalog and memberships. A future seed API or job can replace this mapping without changing stack ownership. Omitting a seed is not an offboarding operation.

A control plane serves all participant seeds in its stack and requires at least one. Data planes and server use-case apps each act as one participant: set participant: <seed-id> on each in a shared stack. With exactly one seed, the CLI selects it automatically. Discovery and data-plane protocol URLs include /tenants/<slug>; management clients send the tenant slug explicitly. See examples/02-ecosystem.yaml and examples/03-managed-service.yaml.

When upgrading an existing control-plane database from before multi-tenancy, its migrated data belongs to the default tenant. Preserve that slug in the participant seed or migrate the tenant explicitly; choosing a new seed id does not move existing data. Tenant bindings select request context; OAuth scopes still govern machine-client access.

HTTP data plane resources

The HTTP data plane executes dataflows. It no longer self-registers datasets or owns their metadata and private asset configuration. Deploy Dataspace Starter next to it to publish and manage those resources through the control-plane SDK:

components:
  controlPlane:
    enabled: true
  dataPlanes:
    http-data-plane: {}
  useCaseApps:
    dataspace-starter: {}

The data plane still stores operational dataflow state and request logs, so its database remains enabled by default.

Use-case apps

A use-case app is an application built on top of a stack, deployed alongside its components under stack.components.useCaseApps. It is user facing, so it gets an Ingress or an HTTPRoute of its own, on the stack's host.

The image profile determines whether an app is a server or static app:

| Shape | What it is | Gets | | -------- | ------------------------------- | -------------------------------------------------------------------- | | server | an API with a web UI | a database, a confidential OAuth client and a rendered config.yaml | | static | a built single-page application | a Deployment, a Service and a route |

First-party profiles are analytics-orchestration (server), dataspace-starter and gaiax-credential-workbench (static). The map key names the rendered resources and the route; type picks the image, so the same app can be deployed twice under different keys.

Dataspace Starter also gets a public OAuth client for its browser-based PKCE login. It has no client secret and no credentials are added to the bundle.

useCaseApps:
  analytics-orchestration: {}
  credentials:
    type: gaiax-credential-workbench
    subPath: /credentials

A server app acts as one participant, selected with participant: <seed-id> in a shared stack. Analytics Orchestration uses an analytics data plane bound to that same participant, when present. A static app has no participant binding and is allowed anywhere. See examples/06-use-case-apps.yaml.

Composition

The CLI applies patches and extra manifests after render and before apply. See examples/05-composition.yaml.

  • Targets are Kustomize-compatible: group, version, kind, name, namespace, labelSelector, annotationSelector.
  • Operations are merge (maps merge recursively, arrays are replaced), jsonPatch (RFC 6902, required for list surgery) and delete: true.
  • Patches run in declared order; global ones first, then a stack's own.
  • Extra manifests load before patches, so a caller can patch their own additions.
  • A target matching zero resources fails unless optional: true; a target matching several fails unless allowMultiple: true.
  • A deleted resource is absent from the bundle, and therefore prune-eligible.

The CLI adds identity labels before patches, which makes them available to a labelSelector. It validates the labels after patching and rejects changes to them.

The typed workload block covers replicas, resources, nodeSelector, routing.className, and routing.annotations. Keep this list small and use patches for less common Kubernetes settings.

Naming

Rendered names are public API, because patches target them. See utils/naming.ts, frozen and tested in test/naming.test.ts. Component resources are <stack>-tsg-<component>, suffixed -config, -ingress, -gateway, -route, -pvc, -role, -binding. Names are namespace-independent, so namespace-per-stack stays addable.

Databases

Every component owns its database through a CNPG DatabaseRole. The login role is named <stack>__<component>, and a separate basic-auth Secret holds its password. Ownership is the isolation boundary, so no component can read another's tables within a stack or across stacks that share a Cluster in database.mode: shared.

PostgreSQL still grants CONNECT on a database to PUBLIC, so any role in the cluster, including CNPG's bootstrap app role, can still connect to any database and read catalog metadata such as table names. Table contents stay owner-only. The CLI does not revoke that grant, because CNPG's Database CR manages schemas and extensions but not privileges, and closing it would mean bootstrap SQL for a gap that exposes no data.

database.mode is still a deployment boundary rather than a hard security boundary: stacks in one namespace share the namespace, and in shared mode they share a PostgreSQL instance and its resources. The default is perStack.

Secrets

Resources generated by the CLI contain no credential values. They only reference existing Secrets for database credentials, OAuth client credentials, private-key JWT keys, the initial administrator password and OID4VCI pre-authorized codes.

Patches, extra manifests and application config are caller-controlled and can still introduce credentials. Inspect those inputs before committing a bundle; the CLI cannot truthfully guarantee that arbitrary composed output is safe for Git.

Deliver Secrets with SOPS, Sealed Secrets, External Secrets, or kubectl. tsg deploy checks that every referenced Secret and key exists before applying anything. For a demo, tsg secrets generate <config> --apply creates them. It does not overwrite material that already exists.

With auth.clientAuthMethod: private_key_jwt, the public JWK is a render input. The renderer cannot read it from the cluster. tsg keys generate writes the public half to keys/<stack>-<client>.jwk to commit, and the private half to a git-ignored directory.

Ownership and pruning

Each apply set has an inventory ConfigMap in the cluster holding the exact identity of every applied object. Labels support status and selection, but the CLI does not use them as deletion authority. A label sweep could delete an unrelated object that received the label through a patch.

The CLI writes the inventory in two phases. Before changing the cluster, it records the previous and desired objects as phase: applying. After apply and readiness succeed, it records only the desired objects and prunes the difference. If readiness fails after apply, fix the cause and rerun the command without a reset.

Normal prune and uninstall retain CNPG Cluster, Database, and DatabaseRole resources plus PVCs. Database and DatabaseRole use a retain reclaim policy. --delete-data changes those policies to delete before removing the custom resources. The CLI works in reverse apply order so it drops each database before its owner role.

An object that another set of the same deployment currently claims is never pruned, no matter how stale the inventory naming it is. Two sets can name the same object after a rename, and ownership by a live set always wins over removal by an obsolete one.

A full deployment reconciles removed stacks and retains their data. A stack-selected deployment does not reconcile unselected stacks. Removing a participant seed does not remove Kubernetes resources.

Inventory writes are optimistic, so a stale invocation cannot prune after another run advanced the same set. Concurrent deployment of the same set is unsupported; different sets are fine.

Code layout

Directories follow the order of the render and deployment pipeline.

bin/tsg-cli.ts    argument parsing only
lib/
  commands/       one file per command group, named after it
  config/         input schema, served versions, conversion
  model/          configuration -> one resolved, complete deployment model
    types.ts        the resolved model
    resolve.ts      topology: stacks, components, images, databases
    profiles.ts     what workload structure each component needs
    appconfig/      each component's application configuration
  render/         model -> typed Kubernetes resources
    sets.ts         apply-set selection and assembly
    workload.ts     Deployment, Service, ConfigMap
    routing.ts      Ingress, Gateway, HTTPRoute
    database.ts     CNPG Cluster, Database, DatabaseRole
    compose.ts      patches, extra manifests, ownership validation
  cluster/        everything that talks to kubectl
    kubectl.ts, preflight.ts, diff.ts, apply.ts, waves.ts, prune.ts,
    inventory.ts, prerequisites.ts
  utils/          generic helpers and frozen contracts
    naming.ts formats.ts version.ts keys.ts release.ts
    log.ts errors.ts objects.ts validation.ts memory.ts
  bundle.ts       the rendered artefact: write, read, verify
  secrets.ts      demo secret material for `tsg secrets` and `tsg keys`
  k8s.ts          resource identity, labels, ordering, serialisation

model/ and render/ may not import anything under cluster/. An ESLint rule enforces the boundary in CI.

Testing

pnpm test

test/examples.test.ts renders every file in examples/, so a patch target that no longer matches a rendered name fails in CI rather than in a user's terminal.

Provisioning on redeployment

Wallet issue configurations are generated from the authority credential type. An issuer can share a wallet with member tenants. An explicit wallet config.issueConfigurations array replaces the generated default.

SSO startup creates missing configured clients by client ID and users by username. Existing passwords, client secrets, keys, permissions and other manually edited settings remain unchanged. Configured redirect URIs are added to existing clients, including browser clients shared by several components. Removing a redirect from configuration does not revoke it; remove it through the SSO management API as well. Persisted client credentials are also used to restore Kubernetes Secrets. Initialization errors fail SSO startup, allowing a restart after the dependency or configuration has been repaired.

Configured wallet tenants are provisioned again at startup. Missing issue configurations and presentation scopes are added by ID and alias respectively; existing entries remain unchanged. Keys, DID documents and their service endpoints remain stored identity defaults. Changing a deployment's public address does not migrate an existing DID or replace its services; use the wallet management API for that deliberate change. A failed tenant initialization is logged and its database transaction rolls back; restart after repair to retry provisioning.

Holder credential claims run after the wallet starts listening. The current bounded retry waits total 25.5 seconds plus request time. Exhaustion logs the tenant failure and exits the wallet so Kubernetes can restart it. Issuer or ingress delays and successful recovery must be verified against the deployed candidate as part of release qualification.