@gainsight-hub/developer-studio-cli
v1.8.2
Published
Gainsight Developer Studio CLI - development tools for widgets, connectors, and pages for the Gainsight CC Widget Catalog
Downloads
3,169
Readme
@gainsight-hub/developer-studio-cli
Command-line tools for building Gainsight CC Widget Catalog widgets, connectors, and pages.
Install
Requires Node.js >= 18 and a CC Widget Catalog account with developer access.
npm install -g @gainsight-hub/developer-studio-cli
gsds --versionQuick start
gsds init acme-widgets && cd acme-widgets
gsds login PAIRING-CODE-FROM-GAINSIGHT
gsds create --name revenue-overview --framework react --category Analytics
gsds previewThen open CC — your local widgets appear in the picker while gsds preview runs.
Authentication
Get a pairing code from your Gainsight community's CLI Access page (Integrations → Developer Studio → CLI Access) and redeem it within its 1-minute TTL. The resulting session token lasts ~8 hours, is tenant-scoped, and is required by both gsds preview and gsds connector test.
gsds login <pairing_code> # redeem a code
gsds login --status # report session state
gsds logout # clear the stored sessionThere is no refresh — re-run gsds login when the session expires. On 401, check gsds login --status. gsds profile marks an expired session (expired), and one whose stored credential can't be read (unreadable), so you can see which tenant to log back into.
Named profiles (multiple tenants)
gsds login stores a session and makes it current, and every other command (preview, logout, connector test) uses it automatically. Logging into a second tenant with a plain gsds login keeps the first session too — each is kept as its own profile, keyed by tenant, and the one you just logged into becomes current.
gsds profile # list stored profiles, marks the current one and any needing a re-login
gsds profile use <name> # switch which one is currentUse --profile <name> on login, preview, logout, or connector test only when you want to name a profile explicitly (e.g. logging into the same tenant twice under different names) or target a non-current profile for a single command without switching it.
Commands
| Command | Purpose |
|---|---|
| gsds init <name> | Scaffold a project, or backfill missing files into an existing widget project (e.g. cloned from widgets-repository-template), migrating a legacy widget_registry.json to extensions_registry.json if that's the only registry present, and adding dedupe.exclude: ["@angular/*"] to a gsds.json that predates it entirely. <name> can be . to scaffold the current directory in place (the project name falls back to the directory's own name). --force for any other non-empty directory. Never overwrites a file it finds |
| gsds create | Scaffold a widget. --name --framework --category |
| gsds list | List widgets, connectors, scripts, stylesheets. --json |
| gsds preview | Dev server + CC pairing. --port --widget <name>... |
| gsds build | Build widgets, validate/normalize the registries. --validate to fail on drift |
| gsds connector test [name] | Run a connector against your tenant |
| gsds script / gsds style | Manage sitewide assets: new link ls rm |
| gsds profile / gsds profile use <name> | List stored login profiles / switch the current one. --json |
| gsds update | Check npm for a newer CLI version now and install it, after confirmation (supplements the automatic check below) |
Frameworks: react, vue, angular, vanilla, html.
gsds create
Interactive by default. In a non-TTY (CI, agent-driven) all three flags are required, and the command fails fast naming what's missing.
Also triggers a registry rebuild so a newly shared dependency is externalized immediately (see below) — but only rebuilds the new widget plus any widget whose externals actually changed this run, not every widget in the project. A registry-rebuild failure only fails create if it's attributable to the widget just created (names it, or names no widget at all); a failure that clearly names a different, already-known widget just warns.
gsds preview
Requires an active session. Each widget needs a dev script in its package.json to appear in the picker; the package manager is resolved from the widget's own package.json packageManager field first, falling back to lockfile detection when that's absent. Widget-service brokers the CC connection via the registered preview session — no token to paste.
gsds build
Builds each widget, then validates and normalizes extensions_registry.json and connectors_registry.json in place. Both are hand-authored sources of truth, not generated output: gsds build never derives widget entries from a widgets/* folder scan, and a widget with no entry is invisible to the platform. Your entries' field order and array order are preserved, so a build with nothing to change produces no diff. Every widget entry in extensions_registry.json must declare a non-empty title and category; the build fails otherwise and names the offending entry. Installs each widget's dependencies automatically first (concurrently) if missing — no manual npm install needed per widget; a widget whose install fails is skipped without blocking the rest.
A leftover per-widget widget.json or connectors.json (from before both registries became the sole source of truth) is never read. gsds build warns naming the widget, and the advice depends on which case you are in:
- the widget is in
extensions_registry.json— the file is redundant, safe to delete. - the widget is not in
extensions_registry.json— thatwidget.jsonis its only remaining metadata. Migrate it into the registry; deleting it loses the widget.
A widgets/<name>/ folder with no registry entry at all is also warned about, since the platform will never serve it.
--validate compares the registries against source and exits 1 on drift — useful as a CI gate. It does not rebuild dist/, so run plain gsds build before committing. It still checks importMaps for drift when every shared dependency's version can be resolved without installing anything; when it can't, it skips that one comparison rather than false-failing.
Shared dependency deduplication
Any package in two or more widgets' package.json dependencies is shared, and gsds build externalizes it out of those widgets' bundles automatically — no author action beyond declaring the dependency. The registry then gets an importMaps entry pointing each shared package at https://esm.sh/<pkg>@<version>, so widgets on the same page load one copy from the browser's module cache instead of one apiece.
A package only one widget uses stays bundled in that widget. Externalizing it would cost a network hop and buy nothing, so nothing changes until a second widget declares it.
Mechanics worth knowing:
gsds buildmay rebuild widgets. When a widget's set of externalized packages changes,gsds buildpatches theconst externalPackages: string[] = [...]declaration in itsvite.config.tsin place (orangular.json'sexternalDependenciesfor Angular) and its bundle is rebuilt so the change takes effect. Nothing else in either file is touched.A version conflict is a hard error. If two widgets sharing a dependency resolve it to different installed versions, the build fails and names every widget/version pair. There is one import map per registry, so one version has to win; align the versions (or remove the dependency from one widget) and re-run.
devDependenciesnever participate — build-time-only packages never reach the browser.Angular widgets are patched through
angular.json'sarchitect.build.options.externalDependencies; every other key in that file is left as you wrote it.@angular/*packages should never be externalized — they ship unlinked partial-Ivy code, and served raw from esm.sh they skip the Angular Linker entirely, breaking at runtime with a JIT-compiler error. There is no working fix for this via import maps today (confirmed: no CDN serves a linked build, Module Federation sidesteps the problem by linking locally rather than publishing a shared linked artifact, and bundling@angular/compilerlocally as a JIT fallback doesn't work either — it and the externally-served@angular/corenever share a module graph). Non-Angular dependencies (rxjs,tslib, or any third-party library two Angular widgets happen to share) dedupe normally.Widgets scaffolded before this feature have a
vite.config.tswith noexternalPackagesdeclaration.gsds buildauto-migrates it into the current shape the first time the widget actually shares a dependency with another widget — nothing to do. It only falls back to a warning (naming the widget, no file touched) when the file's shape can't be confidently patched (a customrollupOptions, or a build block laid out differently than the standard template); copy the externals block from a freshly scaffolded widget'svite.config.tsin that case. A widget whose dependencies nobody else shares is left alone with no warning either way — there's nothing to gain from opting in.HTML widgets have no
package.jsonand never participate.A package (or scope) can be excluded project-wide via
dedupe.excludeingsds.json:{ "dedupe": { "exclude": ["@angular/*", "styled-components"] } }A trailing
/*excludes an entire scope.gsds initseeds this list with@angular/*for the Angular reason above — dedupe has no hardcoded exclusions of its own; every project's protection lives entirely in its owngsds.json. Agsds.jsonthat predates this feature (nodedupekey at all) gets@angular/*backfilled automatically the next timegsds initruns against that project (never touches agsds.jsonthat already has adedupekey, even an empty exclude list). Add further entries for anything else that's safe to bundle per-widget but unsafe to force into a single shared copy, such as a CSS-in-JS library whose independently-built copies don't share a class-name registry, causing style collisions at runtime with no build-time error.dedupe.excludeis the only opt-out — there is no single flag to disable dedupe project-wide. That's by design: dedupe only ever activates for a package two or more widgets actually declare (see above), so excluding the specific packages you don't want shared has the same effect as turning the feature off, without silently reintroducing duplicate bundles the moment a genuinely shareable dependency shows up later.
Automatic updates
gsds keeps itself up to date on its own. Roughly every 8 hours, the next gsds command you run first checks the latest dist-tag on the npm registry, and if a newer version is published it runs npm install -g @gainsight-hub/developer-studio-cli@<version> without asking you to confirm. You'll see two lines when that happens:
ℹ Updating gsds to v1.4.0...
✔ Updated to v1.4.0 - restarting on the new version...The update applies to the command you just typed, not the next one. Once the install finishes, gsds hands the invocation over to the newly installed binary — it re-runs itself from disk with your original arguments and exits with whatever that run returns. So a gsds login that updates on the way in performs the login on the new version, not the old one. This matters for more than freshness: it means the command never runs half on the old code and half against a tree that npm has just rewritten underneath it.
You should not notice the handover beyond the two lines above. Output, exit codes, and Ctrl-C all behave as usual, including for long-running commands like gsds preview.
Only routine upgrades install this way. The automatic path takes a newer version within your current major and never a prerelease. A major bump (1.x → 2.0.0), or a prerelease that has been moved onto the latest tag, is deliberately left for gsds update, so a human sees a potentially breaking version before it lands.
Between checks there is no network call at all: the check state lives in ~/.config/gsds/update-check.json, and the check is skipped outright until 8 hours have elapsed. The timestamp is recorded whatever the outcome (updated, nothing to update, or the check failed), so a registry outage costs one attempt per interval rather than one per command. That file also records the last outcome and the last version installed, which is what to look at first if a machine ends up on an unexpected version.
Parallel gsds invocations (a CI matrix, a Makefile, agent fan-out) coordinate through a lock file next to that cache, so several of them starting at once cannot all launch npm install -g against the same global prefix — one runs the check, the rest skip it.
The check can never fail your command. A network error, a missing npm, an unwritable config directory, or a failed install is swallowed (at most a one-line warning) and your command then runs exactly as it would have — on the current version, with no handover.
Opting out: GSDS_DISABLE_AUTO_UPDATE=1
Set GSDS_DISABLE_AUTO_UPDATE=1 to skip the automatic check entirely — no registry call, no install, no cache write. Use it anywhere an unattended global npm install would do damage or simply fail:
- CI pipelines, where the CLI version should be pinned by the lockfile or install step, not changed underneath a build.
- Sandboxed or offline runners with no registry access, or a read-only
HOME. - Mid-build or agent-driven invocations, where a global install racing the build is a hazard.
export GSDS_DISABLE_AUTO_UPDATE=1It is an escape hatch for those environments, not a general "never update" switch for a normal development machine — with it set you're responsible for running gsds update yourself.
gsds update
Runs the same check immediately and on demand, which is what makes it still useful alongside the automatic one: use it to pull a just-published version now instead of waiting up to 8 hours for the next scheduled check — and it is the only way to take a major-version or prerelease upgrade, which the automatic path skips on purpose.
It compares the running version against the latest dist-tag on the npm registry, then offers to run npm install -g @gainsight-hub/developer-studio-cli@<version> for you. Unlike the automatic check, this command installs nothing without an explicit y at the prompt.
Three things worth knowing:
- It does nothing in CI or any non-interactive shell. With no TTY (or
CI=true) there is no one to confirm a global install, so it prints thenpm install -gcommand for you to run yourself and exits 0 without installing. - The new version applies to the next
gsdsinvocation. This command's process has already loaded its own code and does not hand over to the new one the way the automatic check does. - It installs with npm regardless of how you originally installed
gsds. If you used pnpm, yarn, bun, or Volta, the npm-installed copy may not be the one on yourPATH— re-run the update through your actual package manager instead.
A failed check (registry unreachable, or a response that doesn't contain a usable version) exits 1 and installs nothing.
gsds connector test
gsds connector test [name] [--payload @body.json] [--query k=v] [--path-param k=v] [--verbose] [--json]Resolution is local-first, in two steps: a name is looked up in connectors_registry.json — the root file the platform actually ingests — then against the connector persisted on your tenant. The .gsds/ capture files and --json output record source: local | remote; stderr's source: line names which of the two won.
--payload takes a file reference only (@body.json), and the file must contain valid JSON. --verbose prints the rendered upstream request. Composite connectors cannot be tested.
Runs are captured to .gsds/ (gitignored by gsds init), with sensitive headers redacted.
gsds script / gsds style
Sitewide assets injected on every rendered page, or scoped with --page. new scaffolds a local file, link registers an external CDN URL (must end in .js / .css).
Flags on new / link: --attr k=v, --page <name>, --placement head|bodyStart|bodyEnd (scripts only), --name <slug> (link only). rm --force deletes a non-empty folder.
Automation / AI use
Every interactive path has a flag equivalent and every human output has --json.
gsds create --name revenue-overview --framework react --category Analytics
gsds preview --widget revenue-overview
gsds list --json | jq -r '.widgets[] | select(.previewable) | .type' | xargs gsds preview --widget
gsds connector test revenue-feed --query limit=5 --jsongsds list --json emits an object keyed widgets / connectors / scripts / stylesheets, not a bare array. Commands exit 0 on success and 1 on any failure — treat non-zero as "failed" rather than branching on the value.
Files on disk
Committed, per project: gsds.json (project-root marker), extensions_registry.json, connectors_registry.json, AGENTS.md, README.md, .gitignore.
Gitignored: .gsds/ (connector-test capture output).
Per user: session token stored in the OS keychain (macOS Keychain, Windows Credential Store, or Linux Secret Service / keyutils via @napi-rs/keyring) under service gainsight-developer-studio-cli, one keychain account per profile (default unless --profile <name> was used). On platforms where the keychain is unavailable — no supported provider present, headless CI, or a keychain error — the CLI falls back to ~/.config/gsds/credentials.json (credentials.<profile>.json for a named profile, mode 0600) and warns once on stderr. Set GSDS_DISABLE_KEYCHAIN=1 to force the file-based fallback. Which profile is current is tracked in ~/.config/gsds/current-profile.
Outbound requests
Every request the CLI makes — login redemption, preview sessions, connector tests, and update checks — sends User-Agent: gsds-cli/<version> (node/<version>; <platform>-<arch>). This exists so the platform can see which CLI versions are actually in use and which platforms need support. It carries no user, project, or session identifiers.
Registry data model
Schemas and platform semantics live in the developer portal, not here:
- LLM-optimized: https://developer-portal.gainsight.com/docs/llms-full.txt
- Human-readable: https://developer-portal.gainsight.com/docs
What gsds build reads: each widget's entry in extensions_registry.json (requires title + category; exactly one of source or content), the root connectors_registry.json (connectors and/or composite_connectors, permalinks unique within the file), and each widget's package.json dependencies (to compute shared dependencies). Script and stylesheet entries live only in extensions_registry.json and are managed via gsds script / gsds style.
What gsds build writes into a widget: the externalPackages array in vite.config.ts, or, for Angular widgets, architect.build.options.externalDependencies in angular.json. Both are patched in place — commit the result, don't hand-edit either value.
Troubleshooting
no gsds.json found — run from the project root, or gsds init <name> (or gsds init . for the current directory) first.
session is invalid or expired — ~8 h TTL. Re-run gsds login. Affects preview and connector test.
Pairing code expired — 1-minute TTL. Mint a fresh one from CLI Access and redeem immediately.
Missing required field: category — add a category string to the widget's entry in extensions_registry.json that the error names.
Script "x" points at missing file — a registry entry references a deleted file, and gsds build won't regenerate while inconsistent. Restore the file, or run gsds script rm <name> / gsds style rm <name>. gsds create warns on the same drift without failing so you can still scaffold new widgets while cleaning up the registry.
--payload rejected — file reference only: --payload @body.json, containing valid JSON.
version mismatch across widgets — two widgets share a dependency but resolve different installed versions. The error names each widget and version; align them and re-run gsds build.
excluded from shared-dependency deduplication — that widget's vite.config.ts/angular.json couldn't be auto-migrated (only happens once the widget actually shares a dependency with another and the file's shape can't be confidently patched). Copy the externals block from a newly scaffolded widget's vite.config.ts.
Port in use — gsds preview --port 5200.
Widget missing from the picker — its package.json needs a dev script.
Versioning
Semver. Breaking changes to commands, flags, --json shapes, or the on-disk contract require a major bump. Exit codes are currently only 0 / 1; finer-grained codes may land in a minor.
