Publish a widget from the command line
Log in with the VizChat CLI, preview your widget locally with narrated demos, and publish it to the Marketplace — all from your terminal.
If you develop a widget locally — in your own editor, with your own tooling — the vizchat CLI takes it from a folder on your machine to a published Marketplace entry. This guide covers the commands you need: auth login, widget dev, widget tts, widget publish, and widget merge for when the cloud copy moved while you worked.
Prerequisites: Node.js, a package manager and a VizChat account.
Create the project
npm install -g vizchat
vizchat auth login
vizchat widget create my-widget --mount inlineChoose inline for a React component or nested for DOM code in its own iframe. The command creates the widget in your cloud workspace and syncs a complete local package, including its schema, view, demo, i18n and tests.
Already have a widget package? Then you only need vizchat in its dev dependencies, and a name of the form @<your-username>/<widget-slug>.
cd my-widget && pnpm installWith pnpm 11 or later, an install that pulls in a dependency with a build script it hasn't been told about stops with ERR_PNPM_IGNORED_BUILDS — here that is esbuild, which vizchat uses. The widget works without esbuild's build script, so tell pnpm to skip it in a pnpm-workspace.yaml next to package.json:
allowBuilds:
esbuild: falseor run pnpm approve-builds and pick it. npm doesn't block on this.
The CLI is available inside the project as pnpm exec vizchat; install it globally (npm install -g vizchat) if you also want it outside widget packages. The examples below write plain vizchat.
Commands that act on a widget find its package folder by walking up from the current directory; pass --root <dir> to point at one explicitly.
vizchat widget create also creates the widget's cloud draft and clones it into a local folder. Folders made that way are kept in step with the cloud draft by vizchat workspace sync (see Local workspace CLI).
Log in
vizchat auth loginThe CLI prints a short code and a browser link. Open the link, confirm the code matches, and approve — the terminal picks up the credential automatically. The approval page lists what the CLI asks for: read, write and publish; if this machine shouldn't publish, untick publish (sign in again later to add it). The credential is stored in ~/.config/vizchat/credentials.json, scoped to the server it was issued for, and renews itself; you only sign in again after 30 days without use.
vizchat auth whoami shows who you're logged in as and what was granted; vizchat auth logout revokes this sign-in on the server and deletes it locally. You can also revoke it any time from Settings → Connected apps & devices in the web app.
Preview locally
vizchat widget devThis starts a local preview server and watches your source. The Demo tab plays your demo.stb storyboard; the Interact tab mounts the widget in a live dashboard where you can click and edit it. Source changes recompile and reload automatically.
Add narration audio
Your demo storyboard's markedText narration can be synthesized into audio that plays in the local preview and ships with the published widget:
vizchat widget ttsSynthesis is billed to your account's credits, and only pages whose text changed are synthesized — re-running is free when nothing changed. The audio is kept in .vizchat/local/demo-derived/ inside your package (git-ignored, never compiled into the widget). widget publish uploads it alongside the package, so the published widget reuses it instead of synthesizing — and charging — again. The preview also offers a one-click synthesize bar when pages are missing audio.
Every narrated page needs audio. If some pages fail to synthesize, widget tts keeps the pages that succeeded and exits with an error listing the failed ones — run it again to retry just those (pages already synthesized aren't charged again), or publish and the server synthesizes the missing pages. Slide IDs of narrated pages become part of the audio file path, so they can't contain /, .. or control characters and must be at most 128 characters; widget tts and widget publish reject a demo that breaks this.
Publish
vizchat widget publishThe CLI builds locally first (so obvious errors never leave your machine), makes the widget's cloud draft match your folder, and asks the server to compile, validate, and publish it. The server is the authority: it rebuilds from source and bakes your demo audio into the published version — reusing what you already synthesized locally, at no extra cost. If the widget has a demo, its marketplace cover is rendered from the demo as part of the same publish. Publishing runs on the server as a queued background job; the CLI waits until it has finished (it prints "waiting in the publish queue…" while queued) and only then reports the new version. Before publishing it also scans the source for accidentally committed secrets (see Secret scanning below).
Everything in the package folder is pushed except node_modules/, dist/, dist.prev/, .git/ and .vizchat/ at its root (and the build's own manifest.json). The local demo narration under .vizchat/local/demo-derived/ is not pushed as package source; it is sent separately, as described above. The package can be up to about 18 MB of files; the CLI checks this before sending anything.
Confirm in your browser
Publishing can't be undone. Before a CLI signed in with vizchat auth login publishes, it stops and prints a
confirmation link: open it in a browser (signed in to the same account), check the widget being published — and whether this version's source
will be public — confirm, then run the same command again unchanged. A confirmation covers that one request only. The confirmation link only
appears once the server's checks (including the secret scan below) have passed, so you are never asked to confirm a request that would be
refused. CI access tokens and trusted
publishers were authorized when you created them in Settings and need no confirmation.
The cloud draft moves, too
The push only goes through if the cloud draft is still what you last pushed — the same rule as git push. If someone changed the draft since (an edit in the web editor, or the AI), widget publish does not overwrite it. It compares file by file:
- changed only on your side: kept;
- changed only in the cloud: pulled into your folder;
- changed identically on both sides: nothing to do;
- changed differently on both sides: written to
.vizchat/merge/<id>/{base,local,remote}/<path>, and the command stops withmerge_pending(exit code 10).
For a pending merge, merge the remote/ copies into your working tree (yourself or with your coding assistant), then run:
vizchat widget merge --continue # build, push the merged files, clean up
vizchat widget merge --abort # give up the local mergevizchat widget merge with no flag lists what is pending. While a merge is pending, widget publish refuses to run.
What "last pushed" means depends on how the folder came to be. A folder made by vizchat widget create or vizchat workspace clone is a synced clone: widget publish first runs a workspace sync and the sync engine keeps the baseline. A folder from git clone or a CI checkout has no sync state, so the CLI writes the baseline to .vizchat/base.json after every successful push. Commit that file. Without it a fresh checkout cannot tell "I added this" from "the cloud deleted this", and every differing file is treated as changed on both sides — you are asked to merge. If you forget to commit a newer one, nothing breaks: the extra cloud changes are already in your folder, so they count as identical.
Versions
Every publish creates a new version; a published version never changes afterwards. You don't pick the version number: if the widget's state schema and operations are unchanged, the new version is marked as a fix; otherwise as a feature update. If the new version can no longer read data saved by earlier ones, say so with --breaking:
vizchat widget publish --breakingA new version never reaches documents other people have already made: presentations and dashboards that use your widget stay on the version they had, and their owners see an "updates available" notice and decide for themselves. After you publish a version whose data format changed (a feature update), the publish confirmation on the web lists what is incompatible with the previous version: how many files still on an older version have data that doesn't match, and which fields of your schema are involved — only these totals, never the content of anyone's file. The notice does not block publishing; the owners of those files see it too when they update.
Publishing makes it public
A published widget is immediately visible on the Marketplace — there is no separate "list it" step. The one thing left for you to decide is whether to publish the source code alongside it. That is declared in package.json, not on the command line, and it applies per version:
{
"license": "MIT",
"vizchat": {
"mount": "inline",
"displayName": "My Widget",
"sizeConfig": { "width": 400, "height": 300 },
"openSource": true
}
}Without "openSource": true the source stays closed (the default); leave license out or set it to "UNLICENSED". With it, license must name an open-source license — if the two disagree, the build fails. The publish result tells you whether the version just published is open ("sourceOpen": true, and (source open) in the output).
An open version stays open — you cannot un-see what has been seen. A later version can be closed again by removing the declaration; from then on the Marketplace offers neither the source download nor "Import to my widgets", but the versions already opened stay available under their license. So do a dry run first:
vizchat widget publish --dry-runA dry run builds locally, prints the exact list of files that would be pushed, and stops. It makes no network calls.
To remove a widget from the Marketplace later, use "Take down" on the web — that one is reversible, and independent of whether the source is open.
Secret scanning
Every publish scans the source for likely secrets: the compiled widget is public even when its source is not, so a key baked into the code would leak either way. On a finding nothing is published. If the finding is intentional (a demo API key placeholder, say), re-run once you have reviewed it:
vizchat widget publish --acknowledge-secretsOther commands in the loop
| Command | What it does |
|---|---|
vizchat widget build | Build locally (writes dist/ and manifest.json) |
vizchat widget test | Run the widget's own tests |
vizchat widget lint | Static checks that need no rendering: the schema and dependency declarations |
vizchat widget merge | Show, finish (--continue) or abandon (--abort) a pending merge |
Using it in scripts and CI
Every command writes exactly one line of JSON as the last line of stdout; progress and messages go to stderr, so | tail -1 | jq just works. Exit codes are fine-grained so you can branch on them:
| Code | Meaning | What to do |
|---|---|---|
0 | Success | Continue |
1 | Unexpected failure (internal) | Tell a human |
2 | Usage / argument error (invalid_argument), or the CLI is too old (client_outdated) | Fix the command / upgrade |
3 | Not logged in, or credential expired (auth_required) | Run auth login |
4 | Local validation / build / test failure (invalid_content) | Fix the code |
5 | Needs confirmation (confirmation_required) | Open the printed confirmation link, confirm, and re-run unchanged; for a secret-scan finding re-run with --acknowledge-secrets, for other commands with --yes |
6 | Remote / network error (unavailable) | Retryable |
7 | Not allowed (permission_denied) | Check the target belongs to you |
8 | Not found (not_found) | Check the name |
9 | Conflict (conflict) | Re-read the latest state, or pick another name |
10 | A local merge is pending (merge_pending) | widget merge --continue after merging |
11 | Out of credits or storage (quota_exceeded) | Top up or free space; do not retry |
The last line also carries the lowercase code and a hint with the next step; vizchat --help prints this table.
Commands that spend credits or have irreversible side effects do not proceed silently in a non-interactive shell: widget tts requires an explicit --yes (or use --dry-run to see the quote only).
In CI you don't run auth login; use either credential:
- Access token: issue one under Settings → Connected apps & devices, choosing its permissions, lifetime and the widget it may push, store it as a CI secret and expose it as
VIZCHAT_TOKEN. When it is set the CLI uses it and never reads or writes the local credential file. - Trusted publishing (GitHub Actions, no stored secret): on the same page, register the repository and workflow file (optionally an environment and branch) as a trusted publisher of a widget, give the job
permissions: id-token: write, and runvizchat auth exchangebefore publishing — it trades GitHub's identity token for a 15-minute credential that can only push and publish that widget.
Troubleshooting
- "Not logged in" right after logging in — the credential is scoped per server. If you logged in against a non-default server via
VIZCHAT_API_URL, every command (includingwidget dev) needs the same variable set. - Preview says pages are missing audio but you already synthesized — the preview reads the local audio when the page loads, so after running
widget ttsin a terminal, reload the preview. A page whose narration text changed after it was synthesized stays silent until you synthesize it again. widget publishrejects the author name — the scope in yourpackage.jsonnamemust match your VizChat username exactly.
Publish your work
Put a finished board on the Marketplace — one action, immediately public, with a transcript, play counts, and shareable links.
Use VizChat locally
When something can't run in VizChat's cloud environment — a package that isn't installed, a long job, a shell or Node.js, files on your machine — do it on your own computer against the same workspace, then sync the results back.