Standalone Devtools CLI
Open the complete nice-code devtools without an app or Vite server hosting them. Manage relay sessions, print producer URLs, and remember development credentials in one OS-level data store.
@nice-code/devtools-cli is the preferred way to run devtools when an app’s Vite server is not the right place to host the window. It ships the complete pre-built browser application and works under Node 22+ or Bun. Running it beside a Vite app is fine too: the dev server discovers it and hands devtools clicks to its workspace. See Vite-to-CLI Handoff.
$env:RELAY_ADMIN_SECRET = "<secret>"bunx @nice-code/devtools-cli ` --topology pixel-plaza ` --relay staging=https://staging.devtools-relay.example.com ` --producer-url staging=https://staging.example.com/The CLI opens http://127.0.0.1:5200. Each selected topology, deployed session, or direct backend
gets its own labelled workspace, so commands and clients from unrelated sources can never mix.
If the default port is occupied, a no-argument or default-port launch tries a small following range.
An explicit --port fails unless --fallback-port is passed.
The dashboard: two tasks
Section titled “The dashboard: two tasks”Arguments only set the starting configuration; none are required. Keep the dashboard at / open as
the Workspaces & settings home, or return to it from the ⌂ Workspaces & settings link in any
workspace window’s header. Dashboard and windows are one product, and navigation runs both ways.
The dashboard is organized around the two things you come to do:
- Local development — observe apps running on this machine through the CLI’s local relay. Add a
topology workspace by its exact id; the card then reports the relay and its producers live (see
live status below). Workspaces a Vite dev server registered
through handoff appear here labelled
discovered. - Remote observation — watch a deployed app through an authenticated relay session, or dial a backend directly. Sessions live inside their relay profile’s card.
A first launch with no arguments deliberately starts with no workspace. It asks what you would like to observe, with both paths one click away. Later launches restore only the workspaces you selected before.
Every add, edit and remove is a guided dialog, with no browser prompts, and applies live, only to its own workspace. Removing a workspace only stops observing it; forget removes its local credential; close and forget also closes a deployed relay session.
Connecting a deployed relay: mint or import
Section titled “Connecting a deployed relay: mint or import”The guided flow asks for the relay URL first and probes it before anything is saved. The CLI
fetches <url>/health server-side and checks that the answer identifies a nice-devtools relay. A
URL that answers but is not a relay is refused on the spot, with the probe’s finding. Pasting your
app’s URL here is the classic mistake, and without the probe it would show up later as a confusing
error when minting. A reachability failure can be overridden with Continue anyway; a wrong
identity should be corrected. The profile name is optional and defaults to the relay’s hostname.
From a verified relay you either mint a new session or import one someone else minted. Minting needs the relay’s admin secret; the label is optional and generated from the profile and date. Importing needs the session id and token, no admin secret. Minting ends with the producer URL step and a revealable join URL to share. Importing goes straight to the workspace.
Live status, and what it can prove
Section titled “Live status, and what it can prove”Each local topology card carries two live lines. Both claim only what the CLI can verify.
The relay line distinguishes five states: running and hosted by this CLI, which the CLI can prove because it holds the server; running but hosted by another process (shared), typically a Vite plugin’s relay; the port held by something that is not a devtools relay, found by a service marker check and not a port check (see troubleshooting); not running; or disabled in settings.
The producer line counts who is in the topology’s relay room. Participants declare a diagnostic role when they join: an app bridge joins as a producer, a devtools window as a window. The counts read accordingly: “2 producers connected, 1 window”. These roles are self-declared and used for diagnostics only, never for authentication. A participant that declares none is counted as an unlabelled participant, and a room with only unlabelled participants says so and does not guess (“roles unknown — producers cannot be counted exactly”).
That care is what makes the topology-mismatch detection trustworthy. When producers are
connected on some other room key while your workspace listens on yours, the card says exactly
that: Producers are connected on "tank-shooter"; this workspace listens on "tank-shoter". That is
a proven mismatch. Unlabelled participants on another key are reported only as a possible
mismatch. Against an older or external relay that reports no room diagnostics at all, the card
keeps the plain waiting hint, explicitly labelled as such, and invents no counts.
A workspace card also shows a window open chip while its workspace page is open somewhere. The page heartbeats a server-side lease, released when the page closes, with lease expiry as the fallback. So the chip is trustworthy across tabs, browsers, and crashed windows: it clears within seconds of the window going away.
The Settings section maps every CLI option to an in-app control and labels its source. Explicit CLI
arguments win at startup, followed by environment fallbacks, saved UI preferences, then defaults. A
UI change takes effect immediately where possible. Dashboard host/port, fallback-port, and auto-open
settings are saved for the next launch and stay visibly marked restart required. Local-relay
host/port changes reconnect producers and require confirmation.
--json, --show-credentials, --yes, and help/version would be meaningless as global toggles, so
they map to native actions: copy/export JSON, reveal/reprint, destructive confirmation, and
Help/About.
Topology selection
Section titled “Topology selection”The CLI cannot infer an app’s topology. Pass the exact topologyId used by createNiceDevtools:
bunx @nice-code/devtools-cli --topology pixel-plazaWith fresh state and no workspace option, the CLI selects default. That observes only producers
configured with topologyId: "default"; it does not discover arbitrary topology names.
An empty connected window usually means the producer uses another topology. The dashboard shows the
selected id so you can spot that. Repeat --topology for isolated windows.
The CLI starts the existing local relay on port 5199. A non-default --relay-port must match the
producer’s niceDevtools({ port }) configuration.
Deployed relay sessions
Section titled “Deployed relay sessions”Name every relay explicitly, so profiles and credentials cannot change meaning when the order changes:
nice-devtools session create \ --relay staging=https://relay.example.com/prefix \ --secret-env staging=STAGING_RELAY_ADMIN_SECRET \ --ttl 8h --label "tank staging"The CLI reports the lifetime the relay actually granted. A relay caps --ttl at its own maximum, and
when it shortens your request the CLI prints both: requested 8h 0m, relay granted 2h 0m. New relays return
the exact grant;
against an older compatible relay the remaining duration is labelled approximate. session list
shows sessions known to this nice-devtools installation, not a relay-wide active list.
Use session forget to remove only the local credential. Use session close or the dashboard’s
close and forget to end the relay room and remove it locally. Closing requires an admin secret;
when none is available the dashboard offers only local forgetting.
The session workspace reports connecting, connected, retrying, expired, and proven terminal-close states. A failure before the socket first opens is labelled as ambiguous, because browsers cannot distinguish bad credentials, capacity, origin policy, and relay unavailability.
--producer-url staging=https://staging.example.com/ prints one full URL that preserves the app’s
existing query and hash route. See Remote Devtools Sessions for producer
wiring and production gates.
Plaintext data and removal
Section titled “Plaintext data and removal”For development convenience, remembered session tokens, backend tokens, and opted-in admin secrets
are stored plaintext in state.json:
- Windows:
%LOCALAPPDATA%\nice-devtools - macOS:
~/Library/Application Support/nice-devtools - Linux:
$XDG_STATE_HOME/nice-devtools, then~/.local/state/nice-devtools
Set NICE_DEVTOOLS_DATA_DIR for a portable/test location. nice-devtools data path prints the exact
file. An admin secret or backend token that came from an env variable or a flag is saved only with
--remember-credentials. One you typed at the CLI’s hidden prompt is saved only if you answer yes to
the “remember this plaintext?” question that follows. On a loopback dashboard, you can also enter a credential through a one-way
field. It is cleared after submission and is never prefilled, returned, cached, or stored in browser
storage. You can save an environment-variable name without its value; copying the resolved value to
plaintext requires a separate confirmation. Credential entry and all credential-bearing mutations
remain absent in LAN mode.
A remembered admin secret has broader, longer-lived authority than a session token: it can mint and close sessions until rotated or forgotten locally. The CLI prints where each newly persisted credential was written and the command that removes it.
The dashboard can forget a session, forget credentials for one target, or clear all local data.
CLI equivalents are session forget, credentials forget, and data clear. Forgetting local state
does not close a live relay session unless close and forget is selected.
JSON automation
Section titled “JSON automation”--json writes JSON only to stdout and diagnostics to stderr. Non-TTY mode never prompts.
session create --json includes the new token because automation needs it; session list and
session show --json redact credentials unless --show-credentials is explicit.
LAN mode
Section titled “LAN mode”Credential-bearing workspaces remain loopback-only. A local topology may be exposed deliberately:
nice-devtools --topology pixel --host 0.0.0.0 --relay-host 0.0.0.0The local relay is unauthenticated: anyone on that network can observe application state/actions.
The CLI warns at startup and removes all credential/data controls from the LAN dashboard. A producer
page opened from another device needs niceDevtools({ host: "0.0.0.0", port: 5199 }) too.
A LAN dashboard also omits relay room names and the local-relay status line entirely, because those are disclosed on loopback only. A LAN-mode CLI serves no dev-server discovery or handoff.
One product, one language
Section titled “One product, one language”The dashboard and the devtools window are deliberately built from the same
shared design system. They share one token set, one type ramp (sans for prose, monospace for ids,
URLs, and snippets), and the same accessible primitives: native <dialog> modals that
take focus and close on Escape, labelled form fields, and status lines whose tone is carried in
words (“Error:”, ”✓”, “Note:”) and not by color alone. Arbitrary names such as topology ids, labels
and URLs always render as text, never as markup. If a dashboard screen and a window screen read as
two different products, that is a bug worth reporting.
Troubleshooting
Section titled “Troubleshooting”“Relay URL … did not answer as a nice-devtools relay.” The guided flow probes <url>/health
before saving. This message almost always means the URL is your app’s URL, not the relay’s. A URL
that answers is not necessarily a relay. Enter the relay deployment’s URL (the one your producer
config calls relayUrl). Continue anyway exists for a relay that is temporarily unreachable, not
for a wrong identity.
“Port … is held by something that is not a devtools relay.” The relay status line checks the
service marker, not just the port. Another dev server (or anything else) is sitting on the relay
port; stop it or move the relay port in Settings. The dashboard’s own port follows the
--port / --fallback-port rule from the intro: an explicit port fails when taken,
so a script that pinned a port never silently ends up on another one.
Producers connected, but on a different topology id. The card’s mismatch line quotes both ids.
The fix is making the app’s topologyId and the workspace id exactly equal. They are
case-sensitive, and a one-letter typo is the common case. If the card only reports possible
mismatch, the connected participants declared no role, so the CLI won’t claim more than it knows.
“Waiting for producers … (This relay does not report room diagnostics.)” The workspace is attached to an older or externally started relay that predates room introspection. Everything still works; the card just cannot count producers. Restart the relay from this CLI (or update the process hosting it) to get live counts.
The browser blocked the devtools window. The in-app launcher shows an inline alert with a Retry button. Allow popups for the site and retry from that fresh click. Nothing was registered or half-opened in the blocked attempt.
A devtools click opened the app-local window instead of the CLI workspace. The dev server logs a
stable reason once per cause, such as cli-gone, endpoint-mismatch or announcements-disabled.
The full table, and what each one means, is in
Vite-to-CLI Handoff.