CLI tooling for Polkadot Playground. Installed as the dot command.
curl -fsSL https://raw.githubusercontent.com/paritytech/playground-cli/main/install.sh | bashTo install a specific version:
curl -fsSL https://raw.githubusercontent.com/paritytech/playground-cli/main/install.sh | VERSION=v0.2.0 bashThe installer drops the binary into ~/.polkadot/bin/, symlinks it at ~/.local/bin/dot, appends the path to your shell rc, and then runs dot init so you can finish setup without a second command.
End-to-end first-run setup. Login and toolchain install run concurrently; account setup runs once both have completed successfully.
- Login via the Polkadot mobile app — a QR code is printed to the terminal. Scan it with the app. If you already have a session persisted in
~/.polkadot-apps/, this step is skipped. - Toolchain install —
rustup, nightly,rust-src,cdm, IPFS, andgh. Existing installs are detected and skipped. - Account setup (only if a session is available) — in order:
- Fund — if your balance on Paseo Asset Hub is below 1 PAS, Alice sends 10 PAS (testnet).
- Map —
Revive.map_accountis signed by you on the mobile app so an H160 is associated with your SS58 address. - Allow — Alice grants you 1000 transactions / 100 MB of Bulletin storage.
Flags:
-y, --yes— skip the QR login entirely. Dependencies still install, account setup is skipped (no session).
Self-update from the latest GitHub release. Detects your OS/arch, downloads the corresponding dot-<os>-<arch> asset, verifies HOME is set, and atomically replaces the running binary (write-to-staging-then-rename so the running process is never served a half-written file).
Auto-detects the project's package manager (pnpm / yarn / bun / npm from the lockfile) and runs the build npm script. If no build script is defined, falls back to a framework invocation (vite build, next build, tsc) based on what's installed.
Flags:
--dir <path>— project directory (defaults to the current working directory).
Builds the project, uploads the output to Bulletin, registers a .dot domain via DotNS, and optionally publishes the app to the Playground registry (so it shows up in the user's "my apps" list).
Flags:
--signer <mode>—dev(fast, uses shared dev keys for upload + DotNS — 0 or 1 phone approval) orphone(signs DotNS + publish with your logged-in account — 3 or 4 phone approvals). Interactive prompt if omitted.--domain <name>— DotNS label (with or without the.dotsuffix). Interactive prompt if omitted.--buildDir <path>— directory holding the built artifacts (defaultdist/). Interactive prompt if omitted.--playground— publish to the playground registry so the app appears under "my apps". Interactive prompt (default: no) if omitted.--suri <suri>— override signer with a dev secret URI (e.g.//Alice). Useful for CI.--env <env>—testnet(default) ormainnet(not yet supported).
Passing all four of --signer, --domain, --buildDir, and --playground runs in fully non-interactive mode. Any absent flag is filled in by the TUI prompt.
Requirement: the ipfs CLI (Kubo) must be on PATH. dot init installs it; if you skipped init you can install it manually (brew install ipfs or follow docs.ipfs.tech/install). This is a temporary requirement while bulletin-deploy's pure-JS merkleizer has a bug that makes the browser fallback unusable.
The publish step is always signed by the user so the registry contract records their address as the app owner — this is what drives the Playground "my apps" view.
Fork (or clone) a playground app into a local directory so you can customise and re-deploy it. Fetches the app's metadata from the registry, forks the underlying GitHub repo into your account (falling back to a fresh-history clone if gh isn't authenticated or --clone is passed), runs its setup.sh, and prints next steps.
Flags:
[domain]— positional; interactive picker over the registry if omitted..dotsuffix optional.--clone— clone instead of forking. Skips the repo-name prompt (the target is only a throwaway local directory).--suri <suri>— dev signer secret URI (e.g.//Alice).-y, --yes— skip interactive prompts; use the auto-generated default repo name.--repo-name <name>— repo / directory name; skips the prompt. Validated against GitHub's repository-name rules (letters, digits,.,-,_, not leading with.or-) and rejected if the directory already exists.
When forking, you're prompted for the repo name after picking an app; the default is <slug>-<6 hex chars> and Enter keeps it. Pass --repo-name or -y to run non-interactively.
If dot deploy gets killed with ✖ Memory use exceeded 4 GB (the watchdog's abort) or you see RSS climb unexpectedly, re-run with both of:
DOT_MEMORY_TRACE=1 DOT_DEPLOY_VERBOSE=1 dot deploy ...DOT_MEMORY_TRACE=1streams a per-secondrss / heap / external / peaksample to stderr from the watchdog worker. The worker has its own event loop, so samples keep firing even while the main thread is busy — perfect for capturing the timeline of a leak.DOT_DEPLOY_VERBOSE=1prefixes everybulletin-deploylog line with[+<seconds>s]so you can line the memory samples up with the exact chunk / retry / reconnect that preceded each spike.
Attach the combined output to the bug report along with the site size and roughly how many chunks the deploy was into when the spike started — it's dramatically more useful than a stack trace alone.
pnpm install
pnpm buildCompile and install the dot binary to ~/.polkadot/bin/:
pnpm cli:installpnpm test # one-shot
pnpm test:watch # rerun on change
npx tsc --noEmit # type checkTests live alongside the code as *.test.ts. They avoid mocking so deeply that they just re-implement the code under test — real polkadot-api primitives (Enum) stay real so a variant name change is caught.
Every PR automatically publishes a dev release tagged with the branch name. Others can try it with:
curl -fsSL https://raw.githubusercontent.com/paritytech/playground-cli/main/install.sh | VERSION=dev/my-branch bashReleases are triggered by changesets. To cut a release:
- Create a changeset:
pnpm changeset - Commit the generated
.changeset/*.mdfile with your PR - On merge to
main, CI consumes the changeset, bumps the version, compiles binaries, and creates a GitHub release
Uses Biome. Checked in CI on every PR.
pnpm format # fix
pnpm format:check # check only@polkadot-apps/*are pinned tolatestintentionally — they are our own packages and we want the lockfile to track head.@polkadot-api/sdk-inkis pinned to^0.6.2andpolkadot-apito^1.23.3becausechain-clientcurrently embeds an internalPolkadotClientshape that breaks with newer versions. Bump together withchain-clientonly.bulletin-deployis pinned to an explicit version — notlatest. Currently0.6.9. Previouslylatestpointed at 0.6.8 which had a WebSocket heartbeat bug (40s default < 60s chunk timeout) that tore chunk uploads down asWS halt (3); keeping the pin explicit avoids ever sliding back onto that. When bumping, check the release notes for any changes todeploy()/DotNSAPIs we rely on.
- Single config module (
src/config.ts) — all chain URLs, contract addresses, dapp identifiers and thetestnet/mainnetswitch live here. Nothing else in the tree should hard-code an endpoint or address. - Signer shim (
src/utils/session-signer-patch.ts) — the default session signer from@polkadot-apps/terminalusessignRaw, which the Polkadot mobile app wraps with<Bytes>…</Bytes>(producing aBadProofon-chain). We delegate togetPolkadotSignerFromPjsfrompolkadot-api/pjs-signer, which formats the payload as polkadot.jsSignerPayloadJSON— exactly what the mobile'sSignPayloadJsonInteractorconsumes. This file can be removed once@polkadot-apps/terminaldefaults tosignPayload. - Unified signer resolution (
src/utils/signer.ts) — oneresolveSigner({ suri? })call returns aResolvedSignerwhether the user is authenticated via QR session or a dev//Alice-style URI. Every command threads the result through to its operations instead of branching on source. - Connection singleton (
src/utils/connection.ts) — stores the promise (not the resolved client) so concurrent callers share a single WebSocket. Has a 30s timeout and preserves the underlying error viaError.causefor debugging. - Session lifecycle (
src/utils/auth.ts) —getSessionSigner()returns an explicitdestroy()handle. Callers MUST call it (typically from auseEffectcleanup) — the host-papp adapter keeps the Node event loop alive. - Deploy SDK / CLI split (
src/utils/deploy/+src/commands/deploy/) — the CLI command is a thin Commander + Ink wrapper around a purerunDeploy()orchestrator. The orchestrator avoids React/Ink so WebContainer consumers (e.g. RevX) can drive their own UI off the same event stream. - Signer-mode isolation (
src/utils/deploy/signerMode.ts) — decides which signer each deploy phase uses (pool mnemonic vs user's phone) in one place so the mainnet rewrite can be a single-file swap. - Bulletin delegation — all storage-side hardening (pool management, chunk retry, nonce fallback, DAG-PB verification, DotNS commit-reveal) stays inside
bulletin-deploy. We calldeploy(..., { jsMerkle: true })so the flow stays binary-free and runs unchanged in a WebContainer. - Signing proxy (
src/utils/deploy/signingProxy.ts) — wraps the user'sPolkadotSignerto emitsign-request/-complete/-errorlifecycle events. The TUI renders these as "📱 Check your phone" panels with live step counts. - Playground publish is ours (
src/utils/deploy/playground.ts) — we deliberately do NOT usebulletin-deploy's--playgroundflag. We call the registry contract fromsrc/utils/registry.tswith the user's signer so the contract records theirenv::caller()as the owner — required for the Playground app's "my apps" view.