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

@gainsight-hub/hubforge

v0.16.0

Published

DCH development tools for widgets, connectors, and pages

Downloads

511

Readme

@gainsight-hub/hubforge

CLI for building and previewing widgets for the Gainsight CC Widget Catalog.

npm version node

Prerequisites

  • Node.js >= 18
  • A CC Widget Catalog account with developer access

Installation

npm install -g @gainsight-hub/hubforge

Verify:

hf --version

Both hf and hubforge are available as command aliases.

Quick Start

# 1. Scaffold a new widget
hf create

# 2. Start the dev server
hf preview

# 3. In your CC instance, click "Edit Layout" → "Dev Mode" → enter the port and token → click Connect

Commands

hf create

Scaffold a new widget interactively.

hf create [options]

| Flag | Description | Default | | ------------------------- | ---------------------------------------------------------------- | -------- | | --name <slug> | Widget identifier — lowercase, alphanumeric, hyphens/underscores | prompted | | --framework <framework> | One of: react, vue, angular, vanilla, html | prompted | | --category <name> | Widget category shown in CC Widget Catalog | prompted |

Example (interactive):

? Widget name (e.g. my-dashboard-widget) › revenue-overview
? Framework › React
? Category › Analytics

Creates widgets/revenue-overview/ and registers the widget in widget_registry.json (auto-created if missing).

Example (non-interactive):

hf create --name revenue-overview --framework react --category Analytics

hf preview

Start a local dev server and pair it with CC Widget Catalog.

hf preview [options]

| Flag | Description | Default | | ----------------- | ----------------------------------------------------------------------- | -------- | | --port <number> | Port for the HubForge server — overrides HF_PORT | 5173 | | --widget <name...> | Preview only the named widget(s); skips the interactive picker | prompted | | --verbose | Show detailed dev server startup diagnostics | — |

Use --widget to select widgets non-interactively — useful for scripting and automation. Pass it one or more times (or space-separated):

hf preview --widget revenue-overview
hf preview --widget revenue-overview --widget static-banner

Widget names are slugs (the type field / widgets/<name>/ directory). Unknown or non-previewable names fail with an error listing the available widgets. Run hf preview with no flags for the interactive picker.

| Environment variable | Description | Default | | -------------------- | ----------------------------------------------------- | ------- | | HF_PORT | Port for the HubForge server (overridden by --port) | 5173 | | HF_TIMEOUT | Seconds to wait for each widget's dev server to start | 60 |

Run from your project root — the directory containing widget_registry.json. Running from a subdirectory will fail.

Each widget must have a dev script in its package.json to appear in the preview selector. Widgets without one are skipped.

HubForge auto-detects the package manager for each widget (bun, yarn, pnpm, or npm) based on the lockfile present.

Example output:

  ╭────────────────────────────────────────────────────────────────────╮
  │                                                                    │
  │  Widget Dev Preview                                                │
  │                                                                    │
  │  Server:  http://localhost:5173                                    │
  │  Token:   A3KP-8XZQ                                               │
  │                                                                    │
  │  Open CC Widget Catalog, enter the port and token above.          │
  │  Press Ctrl+C to end session.                                      │
  │                                                                    │
  ╰────────────────────────────────────────────────────────────────────╯

The token is a single-session secret — it changes every time you run hf preview.


hf list

List all discovered widgets and whether each can be previewed.

hf list [options]

| Flag | Description | Default | | -------- | ------------------ | ------- | | --json | Output as JSON | — |

Discovers every widgets/*/ directory with a widget.json and reports each widget's kindjs (has a dev script), html (static source, no build), or skipped (neither, so not previewable).

Human output:

  Widgets (3)

  ✔  revenue-overview  js       Revenue Overview
  ✔  static-banner     html     Static Banner
  ⚠  legacy-thing      skipped  Legacy Thing

JSON output (--json) — useful for scripting; pair with hf preview --widget <type>:

[
  { "type": "revenue-overview", "title": "Revenue Overview", "kind": "js", "previewable": true },
  { "type": "static-banner", "title": "Static Banner", "kind": "html", "previewable": true },
  { "type": "legacy-thing", "title": "Legacy Thing", "kind": "skipped", "previewable": false }
]

hf login

Authenticate the CLI with a pairing code issued by the Gainsight app.

hf login <pairing_code>
hf login --status

| Argument / Flag | Description | Default | | ----------------- | ---------------------------------------------------------------- | ------- | | [pairing_code] | One-time code shown by the Gainsight "Connect CLI" flow | — | | --status | Show the current session (tenant + expiry) without authenticating | — |

hf login <pairing_code> redeems a code. hf login with no code reports the current session if one is valid, otherwise asks for a code. hf login --status always reports session state (and cannot be combined with a code).

In your Gainsight instance, open the developer settings and click Connect CLI to mint a short-lived pairing code, then redeem it:

hf login PAIRING-CODE-FROM-GAINSIGHT

On success the CLI stores a short-lived (~8h), tenant-scoped token in ~/.config/hubforge/credentials.json (owner-readable only) and reports the tenant and when the token expires. Other commands (e.g. hf connector test) pick this token up automatically. When it expires, run hf login again — there is no refresh token.

The token is scoped to the tenant that issued the pairing code. Run hf login --status to see the current tenant and expiry without re-authenticating.


hf logout

Remove the stored CLI session credentials.

hf logout

Deletes ~/.config/hubforge/credentials.json. Reports whether a session was present.


hf connector test

Run a connector against your tenant using its definition, and print the response.

hf connector test [name] [options]

| Flag | Description | Default | | ------------------- | ----------------------------------------------------------------------- | -------------------- | | [name] | Connector name or permalink; required only when more than one found | single auto-selected | | --config <file> | Read connectors from this file instead of auto-discovering | auto-discover | | --payload <json> | Request body for POST/PUT/PATCH connectors | — | | --query <kv> | Query parameter key=value (repeatable) | — | | --path-param <kv> | Path parameter key=value (repeatable) | — | | --method <verb> | Override the HTTP method — local connectors only | connector's own | | --dev-key <key> | Developer API key — overrides hf login session and HF_DEV_API_KEY | stored session | | --timeout <s> | Abort the request after N seconds | 30 | | --verbose | Also print the rendered upstream request | — | | --json | Print the full result record as JSON | — |

Where the definition comes from

Resolution is local-first. A name is looked up in widgets/*/connectors.json and, if it is not there, tested against the connector persisted on your tenant — resolved by permalink, or by numeric id. This is how you test a registry-synced connector that was never authored locally, and how you check what your tenant actually runs versus what your working copy says.

Every run reports which definition it used on stderr, so the two are never confused:

auth: hf login session · tenant acme
source: widgets/*/connectors.json      # your working copy
source: persisted on your tenant       # the stored definition

The same value is recorded as source: "local" | "remote" in the --json output and the capture files. The 404 and expired-session hints are printed for human output only; --json stays machine-parseable.

Some cases deliberately do not fall back, because "absent from the working copy" would be indistinguishable from "the working copy is not visible":

  • --config <file> is an explicit request for that file.
  • A connectors.json that fails to parse or validate is reported rather than masked by the stored definition.
  • Running from a subdirectory of a project errors and asks you to run from the project root. Discovery is relative to the current directory, so a subdirectory sees none of your widgets. This is detected via the hubforge.json marker written by hf init. If no marker is found and the current directory holds a connectors.json, the source: line says project scope could not be verified rather than falling back silently. --config from a subdirectory works as normal.
  • A locally authored composite connector is reported as unsupported rather than looked up on your tenant.

A numeric reference is always resolved as a connector id, never a permalink — so an all-digit permalink cannot be targeted remotely.

Because a persisted connector carries its own HTTP method, --method applies only to a locally defined one. --payload, --query and --path-param work on both. Composite connectors are not supported either way.

The developer key is resolved by precedence: --dev-keyHF_DEV_API_KEY → the token stored by hf login. An expired stored token prompts a re-login rather than a failed request.

hf login PAIRING-CODE-FROM-GAINSIGHT
hf connector test revenue-feed --query limit=5   # local if present, otherwise your tenant's

Each run is written to .hf/last-connector-result.json and appended to .hf/connector-results.jsonl in the project root (sensitive headers redacted, large bodies truncated). Add .hf/ to your project's .gitignore so captured responses are never committed.


hf build

Build extensions_registry.json and connectors_registry.json from source. Runs from the project root.

hf build             # regenerate both registries
hf build --validate  # rebuild in-memory and diff against on-disk; exit 1 on drift
hf build --strict    # fail on missing widget category (do not apply DEFAULT_WIDGET_CATEGORY)

| Flag | Description | | ------------ | ---------------------------------------------------------------------------------------- | | --validate | Rebuild in-memory and compare against the on-disk registries. Non-zero exit on any drift. | | --strict | Fail if any widget omits category instead of applying DEFAULT_WIDGET_CATEGORY. |

Sources scanned:

  • widgets/<name>/widget.json → aggregated into extensions_registry.json (widgets array).
  • widgets/<name>/connectors.json → aggregated into connectors_registry.json (connectors and/or composite_connectors). Either or both arrays are accepted.
  • scripts/<name>/script.json → aggregated into extensions_registry.json (scripts array). Local files or external CDN URLs (see below).
  • stylesheets/<name>/stylesheet.json → aggregated into extensions_registry.json (stylesheets array). Local files or external CDN URLs.

External URL scripts and stylesheets

A script or stylesheet descriptor may reference an external CDN URL directly via a path field. When present, no local .js/.css file is required in the descriptor directory.

{
  "name": "gtag",
  "description": "Google Analytics",
  "path": "https://cdn.jsdelivr.net/npm/[email protected]/dist/hello.js",
  "attributes": { "async": "" }
}

The path field must start with http:// or https:// and must end in .js (for scripts) or .css (for stylesheets), matching the widget-service-documented pattern. path and contentFile are mutually exclusive — omit path for repo-hosted files.

Composite connectors

connectors.json may declare a composite_connectors array alongside (or instead of) connectors. Composites define an ordered sequence of steps:

{
  "composite_connectors": [
    {
      "name": "Country Lookup",
      "permalink": "country-lookup",
      "steps": [
        { "name": "all-countries",    "url": "https://restcountries.com/v3.1/all",           "method": "GET" },
        { "name": "europe-countries", "url": "https://restcountries.com/v3.1/region/europe", "method": "GET" }
      ]
    }
  ]
}

Composite permalinks share the connector namespace and must be unique across all widgets. Each step requires a slug name, a url, and optionally method, headers, query_parameters, authentication, request_body, response_body, response_content_type. path_parameters are not supported on steps.


Pairing with CC Widget Catalog

When hf preview is running:

  1. Open your CC instance and click Edit Layout
  2. Click Dev Mode in the widget sidebar
  3. A Connect Dev Server dialog appears with Port and Token fields
  4. Enter the port (default 5173) and paste the token shown in the terminal
  5. Click Connect — your local widgets appear in the catalog

Project Structure

my-project/
├── widget_registry.json        # Widget registry (managed by hf create)
└── widgets/
    └── my-widget/
        ├── package.json        # Must include "dev" script for preview
        ├── src/
        ├── dist/               # Output of build script
        └── thumbnail.png

widget_registry.json

HubForge reads widget_registry.json from the project root to discover widgets. hf create manages this file automatically.

JS widget entry (React, Vue, Angular, Vanilla):

{
  "type": "revenue-overview",
  "title": "Revenue Overview",
  "version": "1.0.0",
  "category": "Analytics",
  "imageSrc": "widgets/revenue-overview/thumbnail.png",
  "widgetsLibrary": true,
  "containers": ["Full width"],
  "settings": {
    "configurable": true,
    "editable": true,
    "removable": true,
    "shared": false,
    "movable": false
  },
  "source": {
    "path": "widgets/revenue-overview/dist",
    "entry": "index.html"
  },
  "configuration": {
    "properties": [
      {
        "name": "title",
        "type": "text",
        "label": "Title",
        "defaultValue": "Revenue Overview",
        "rules": { "required": true }
      },
      {
        "name": "description",
        "type": "text",
        "label": "Description",
        "defaultValue": ""
      }
    ]
  },
  "defaultConfig": {
    "title": "Revenue Overview",
    "description": ""
  }
}

Static HTML widget entry (no build step):

{
  "type": "static-banner",
  "title": "Static Banner",
  "version": "1.0.0",
  "category": "My Widgets",
  "imageSrc": "widgets/static-banner/thumbnail.png",
  "widgetsLibrary": true,
  "containers": ["Full width"],
  "settings": {
    "configurable": true,
    "editable": true,
    "removable": true,
    "shared": false,
    "movable": false
  },
  "source": {
    "path": "widgets/static-banner/dist",
    "entry": "content.html"
  }
}

Supported Frameworks

| --framework | Description | | ------------- | --------------------------------------------------- | | react | React with Vite | | vue | Vue 3 with Vite | | angular | Angular with zoneless change detection | | vanilla | Vanilla JS/TS with Vite | | html | Static HTML — no build step, no dev server required |

Environment Variables

| Variable | Affects | Default | Description | | ----------------- | ---------------------------------- | ------------------------------ | ------------------------------------------------------------- | | HF_PORT | hf preview | 5173 | HTTP server port | | HF_TIMEOUT | hf preview | 60 | Seconds to wait for a widget's dev server to become reachable | | HF_DEV_API_KEY | hf connector test | — | Developer API key; overridden by --dev-key, falls back to the hf login session |

Troubleshooting

Port already in use Use --port or HF_PORT to pick a different port:

hf preview --port 5200

Widget not appearing in the preview selector The widget's package.json must have a dev script. Check with cat widgets/<name>/package.json.

Dev server times out Increase the timeout: HF_TIMEOUT=120 hf preview. Also verify the widget's dev script works on its own by running it directly from widgets/<name>/ with your package manager (e.g. npm run dev, bun run dev).

"widget_registry.json not found" Run hf commands from the project root — the directory that contains widget_registry.json.

Pairing token not accepted The token is tied to the current session. Restart hf preview and reconnect via Edit Layout → Dev Mode with the new token.