Standalone Devtools CLI
Open complete nice-code devtools without an app/Vite host, manage relay sessions, print producer URLs, and remember development credentials in one OS-level data store.
@nice-code/devtools-cli is the preferred operator front door 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 is
its own labelled workspace, so commands and clients cannot cross unrelated provenance boundaries.
If the default port is occupied, a no-argument/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 are an initial configuration overlay, not a requirement for using the tool. Keep the
dashboard at / open as the Workspaces & settings control plane, 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 actually 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.
An argument-free first launch intentionally begins with no workspace and asks what you would like to observe, with both paths one click away. Later launches restore only the workspaces selected previously.
Every add/edit/remove is a guided dialog — no browser prompts — and applies live, isolated by workspace provenance. 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 — pasting your app’s URL here is the classic mistake — is
refused on the spot with the probe’s finding, instead of surfacing 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 (needs the relay’s admin secret; the label is optional and generated from the profile and date) or import one someone else minted (session id + token — no admin secret involved). 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, and both are careful to only claim what the CLI can actually verify:
The relay line distinguishes five states: running and hosted by this CLI (ownership the CLI can prove — it holds the server); running but hosted by another process (shared) — a Vite plugin’s relay, typically; the port held by something that is not a devtools relay (a service marker check, not a port check — see troubleshooting); not running; or disabled in settings.
The producer line counts who is actually 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 — and the counts read accordingly: “2 producers connected, 1 window”. These roles are self-declared and are used for diagnostics only, never for authentication; a participant that declares none is counted honestly as an unlabelled participant, and a room with only unlabelled participants says so rather than guessing (“roles unknown — producers cannot be counted exactly”).
That honesty 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". — which is a
proven mismatch. Unlabelled participants on another key are reported only as a possible mismatch.
And against an older or external relay that reports no room diagnostics at all, the card keeps
the plain waiting hint, explicitly labelled as such, rather than inventing counts.
A workspace card also shows a window open chip while its workspace page is open somewhere. This is a server-side lease the page heartbeats, 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 actually 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 remain visibly marked restart required; local-relay
host/port changes reconnect producers and require confirmation.
--json, --show-credentials, --yes, and help/version use native actions rather than meaningless
global toggles: 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, which 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 and keeps that distinction visible. 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 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 relay’s effective lifetime, including a clamp. New relays return the exact grant;
against an older compatible relay the remaining duration is labelled approximate. session list
means 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 truthfully 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. Admin/backend values are copied from an env/flag only with --remember-credentials; a hidden
prompt asks explicitly. On a loopback dashboard, a credential can also be entered through a one-way
field. It is cleared after submission and is never prefilled, returned, cached, or stored in browser
storage. An environment-variable name can be saved 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 — those are a loopback-only disclosure — and 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 — 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:”) rather than color alone. Arbitrary names — topology ids, labels, URLs — always render as
text, never as markup. If a dashboard surface and a window surface 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 —
answering is not the same as being 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 squatting the relay
port; stop it or move the relay port in Settings. Similarly for the dashboard itself: a default
launch tries a small following port range, but an explicit --port fails when the port is taken
unless --fallback-port says falling forward is acceptable — a script that pinned a port should not
silently end 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 — cli-gone, endpoint-mismatch, announcements-disabled, and
friends. The full table and what each one means is in
Vite-to-CLI Handoff.