Sync runs to the Plune platform
Four commands run fully local — run, report, diff and init: no account, no network, no data
leaving your machine. The rest talk to a server and are entirely opt-in: login, logout, sync,
ingest, and the plune run start | finish | exec | report | import group that reports a suite’s
results to the platform. You can use Plune forever without any of them.
This list grew: until 0.7.0 it really was three. Naming the local four is the more durable way round — the local half is the promise, and it is the half that has not changed.
What they add is history. plune run writes its result to .plune/last-run.json and that is where it
stays; plune sync uploads that file to the Plune platform, where runs accumulate into a pass-rate and
cost trend you can look at over time.
Get a token
Section titled “Get a token”-
Sign in to the dashboard — it uses a magic link, so there is no password to manage.
-
Open Account → API tokens — the avatar menu at the top right → Account — pick the project the runs belong to and generate one. The token is shown once; copy it then.
-
Hand it to the CLI (see below). Tokens are per-user and can be revoked from the same screen.
plune login
Section titled “plune login”Saves the token so every later command can use it.
plune login --token plune_xxxxxxxxPassing a secret as an argument leaves it in your shell history, so login also reads the token from
standard input — piped or pasted:
echo "$PLUNE_TOKEN" | plune login # scripted / CIplune login # interactive: paste it, press EnterThe token is written to ~/.config/plune/credentials.json (honouring XDG_CONFIG_HOME) with 0600
permissions inside a 0700 directory. The command prints only the path — the token itself is never
echoed to a terminal or a log:
Logged in. Token saved to /home/you/.config/plune/credentials.jsonAn empty token exits 2 rather than saving an unusable file.
plune logout
Section titled “plune logout”Removes the saved token. It is safe to run when you are not logged in — that is not an error:
plune logout# Logged out. Removed /home/you/.config/plune/credentials.json# …or: Not logged in — nothing to remove.plune sync
Section titled “plune sync”Uploads a run to POST /v1/runs and prints the id the server assigned, plus the URL to read it back:
plune run # produces .plune/last-run.jsonplune sync # uploads itBy default it syncs the most recent run. Point it at a specific file to upload an older or archived one:
plune sync --file reports/nightly-2026-07-27.jsonThe global -c, --config flag works the way it does for plune report: it makes sync look for the run
saved next to that config rather than in the current directory.
plune -c packages/api/plune.yaml syncThe uploaded file is the run exactly as the CLI wrote it — the platform validates it against the same
frozen RunResult contract the CLI produces, so nothing is reshaped on the way out.
Exit codes
Section titled “Exit codes”sync separates “you need to do something” from “the environment failed”, so a CI step can react to the
difference instead of retrying a hopeless upload:
| Code | Meaning | Typical cause |
|---|---|---|
0 | Uploaded | — |
2 | Your input needs fixing | Not logged in, token rejected or revoked, run file missing or invalid |
1 | Environment or server | Network unreachable, API returned 5xx |
Errors are printed as a single readable line; the token never appears in the output, and neither does a stack trace.
sync reads PLUNE_TOKEN from the environment — or from a .env in the current directory, from
0.16.0 — and the saved login only when the variable is set nowhere. So a CI job hands it the secret
directly, with no login step:
- name: Upload the run to Plune env: PLUNE_TOKEN: ${{ secrets.PLUNE_TOKEN }} run: | npx -y @plune-ai/cli run npx -y @plune-ai/cli syncBefore 0.13.0 sync read the credentials file and nothing else, and a job logged in first, from a
secret — which is what the stdin form of plune login is for.
Programmatically
Section titled “Programmatically”The same upload is exported from the package, alongside typed errors — so a caller can tell “log in” apart from “the network is down” without parsing messages:
import { run, sync, NotLoggedInError, SyncNetworkError } from "@plune-ai/cli";
await run({ configPath: "plune.yaml" });
try { const { id, url } = await sync(); console.log(`uploaded ${id} → ${url}`);} catch (err) { if (err instanceof NotLoggedInError) { // ask for a token, then retry } else if (err instanceof SyncNetworkError) { // transient — safe to retry later } else { throw err; }}sync() accepts the same choices the flags expose (file, apiUrl, and injectable fetch/token
loading for tests), and reportSyncFailure(err, write) maps any of the typed errors to the exit code
the CLI itself would use.