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.--modable/--no-modable— publish the source repo URL alongside the deploy so others candot modit. Requires--playground. Interactive prompt (default: no) if omitted. When set,dot deployuses the existingoriginURL without pushing. If there is noorigin, it ensuresgitandghare installed (auto-installs if missing), confirmsghis authenticated (rungh auth loginfirst if not — the deploy will fail with a hint otherwise), then runsgh repo create --public --pushto set one up. The resulting URL is recorded in the Bulletin metadata.--repo-name <name>— repo name to use when--modableneeds to create a new GitHub repo (no existingorigin). Defaults to the basename of the project directory; validated against GitHub's repository-name rules.--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. --modable is independently optional in both modes — its absence means a non-modable deploy.
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.
Pull a modable playground app's source into a fresh local project so you can customise and re-deploy it. The interactive picker only shows apps that opted into modable at deploy time; non-modable apps surface a clear "this app is not modable" error if you target them by domain.
The implementation is GitHub-only and requires no CLI tooling — neither git nor gh is needed. Source is downloaded as a tarball over HTTPS from codeload.github.com (no auth needed for public repos), extracted into the target dir, then git init'd as a fresh history if git happens to be on PATH. With git absent, the directory still works — you just don't get version control until you install git yourself.
Flags:
[domain]— positional; interactive picker over the registry if omitted..dotsuffix optional. The picker is filtered to modable apps only.--suri <suri>— dev signer secret URI (e.g.//Alice).
The local directory name is auto-generated as <slug>-<6 hex chars> so repeated mods of the same starter never collide (unlike GitHub forks, which were limited to one per account per repo).
Telemetry is off by default for unknown external users. It is enabled automatically in known internal Parity contexts and can be controlled explicitly:
DOT_TELEMETRY=1opts in.DOT_TELEMETRY=0opts out.DOT_TAG=e2e-local-smokemarks a synthetic run; use the samee2e-*,load-*, orcanary-*naming families for other synthetic traffic.SENTRY_DSNoverrides the bundled Sentry DSN for local testing.
Telemetry scrubs local home-directory paths and avoids sending raw command arguments.
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 # unit tests, one-shot
pnpm test:watch # rerun on change
npx tsc --noEmit # type check
pnpm test:e2e # E2E tests (slow; run `dot init` first)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.
Live under e2e/cli/*.test.ts, with a separate e2e/vitest.config.ts. Each test spawns the CLI via bun run src/index.ts (execa wrapper in e2e/cli/helpers/dot.ts) and asserts on stdout/stderr/exit code. Files run serially — they share a single deployer account on Paseo and would race otherwise.
Prerequisite: run dot init once to install the required local deps (mainly Kubo IPFS for the deploy pipeline). Tests also reach Paseo Asset Hub and codeload.github.com over the internet, so they need network.
CI runs the suite on every PR, on push to main, and daily at 06:00 UTC (.github/workflows/e2e.yml).
Running a single file: invoke vitest directly to avoid a pnpm/vitest ---forwarding gotcha that runs the whole suite anyway:
pnpm vitest run --config e2e/vitest.config.ts e2e/cli/session.test.tsEvery 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.7.6. 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.