Skip to content

Sync runs to the Plune platform

Everything else in the CLI runs fully local: no account, no network, no data leaving your machine. login, logout and sync are the only three commands that talk to a server, and they are entirely opt-in — you can use Plune forever without them.

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.

  1. Sign in to the dashboard — it uses a magic link, so there is no password to manage.

  2. Open Settings → API tokens and generate one. The token is shown once; copy it then.

  3. Hand it to the CLI (see below). Tokens are per-user and can be revoked from the same screen.

Saves the token so every later command can use it.

Terminal window
plune login --token plune_xxxxxxxx

Passing a secret as an argument leaves it in your shell history, so login also reads the token from standard input — piped or pasted:

Terminal window
echo "$PLUNE_TOKEN" | plune login # scripted / CI
plune login # interactive: paste it, press Enter

The 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.json

An empty token exits 2 rather than saving an unusable file.

Removes the saved token. It is safe to run when you are not logged in — that is not an error:

Terminal window
plune logout
# Logged out. Removed /home/you/.config/plune/credentials.json
# …or: Not logged in — nothing to remove.

Uploads a run to POST /v1/runs and prints the id the server assigned, plus the URL to read it back:

Terminal window
plune run # produces .plune/last-run.json
plune sync # uploads it

By default it syncs the most recent run. Point it at a specific file to upload an older or archived one:

Terminal window
plune sync --file reports/nightly-2026-07-27.json

The 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.

Terminal window
plune -c packages/api/plune.yaml sync

The 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.

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:

CodeMeaningTypical cause
0Uploaded
2Your input needs fixingNot logged in, token rejected or revoked, run file missing or invalid
1Environment or serverNetwork 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.

There is no environment-variable fallback for the token — sync reads the credentials file and nothing else. So a CI job logs in first, from a secret, which is exactly what the stdin form is for:

- name: Upload the run to Plune
env:
PLUNE_TOKEN: ${{ secrets.PLUNE_TOKEN }}
run: |
npx -y @plune-ai/cli run
echo "$PLUNE_TOKEN" | npx -y @plune-ai/cli login
npx -y @plune-ai/cli sync

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.