pscli is a small Go CLI for querying Perfectscale's public API with a UI-generated service token.
It is optimized for fast terminal exploration and agent-friendly output, with:
- stored local auth profiles
- sensible production defaults
- table, JSON, and JSONL output
- workload filtering, sorting, aggregation, and CSV export
- GitHub Actions builds for macOS, Windows, and Linux
- Homebrew install via
brew install doitintl/tap/pscli - Scoop install on Windows via
scoop bucket add pscli ... && scoop install pscli .deb/.rpmpackages for direct install on Linuxpscli skill <agent>to install the Perfectscale coding-agent skill
This CLI is intentionally public-API only. Command groups: auth,
clusters, namespaces, workloads, nodegroups, unevictable,
automation, plus update, skill, and commands — see their dedicated sections
below for subcommands and flags.
The CLI uses Perfectscale public API service tokens from the Perfectscale UI.
To generate one:
- Open
app.perfectscale.io - Click the user circle in the bottom left
- Open
Org Settings - Open
API Tokens - Click
Generate Token - Assign a
Read Onlyrole - Copy the
client_idandclient_secret
If you run pscli auth without saved credentials, the CLI prints the same setup guide plus a ready-to-copy login example.
Then log in:
pscli auth loginOr pass the credentials directly:
pscli auth login --client-id 'YOUR_CLIENT_ID' --client-secret 'YOUR_CLIENT_SECRET'The CLI validates the token by calling the public auth endpoint and saves the profile locally.
The CLI is designed so common usage does not require flags.
Default profile:
default
Default output:
table
Default public API base URL:
https://api.app.perfectscale.io/public/v1
If you do not export any environment variables and do not pass any global flags, the CLI will talk to production.
Global flags can be set through environment variables:
PERFECTSCALE_PROFILEPERFECTSCALE_OUTPUTPERFECTSCALE_DEBUGPERFECTSCALE_PUBLIC_API_URL
Examples:
export PERFECTSCALE_PROFILE='dev-public'
export PERFECTSCALE_PUBLIC_API_URL='https://api.dev.perfectscale.click/public/v1'
export PERFECTSCALE_OUTPUT='jsonl'-o json/-o jsonl (or PERFECTSCALE_OUTPUT=json/jsonl) are explicit,
deliberate choices — there's no auto-detection of "an agent is driving this."
Agents and humans alike get exactly what they ask for: pass -o (or set
PERFECTSCALE_OUTPUT) explicitly for machine-readable output, and leave it
unset for the default table.
-o json is a single, pretty-printed document — meant to be read.
-o jsonl is a compact stream, one object per line — meant to be parsed
(jq -c per line, jq -s to collect). A command whose result isn't
list-shaped falls back to a single compact JSON document under -o jsonl,
matching jsonl's own convention.
On failure, the error is printed as a JSON object on stderr instead of the usual plain-text message, following the same pretty-vs-compact split:
{"error":{"code":"RESOURCE_NOT_FOUND","message":"cluster \"prod-a\" not found","retryable":false}}code is a stable machine-readable string; retryable says whether
retrying the same request might succeed (true for rate limits and 5xx
responses). The process exit code is also mapped to the error category —
2 for a usage/flag mistake, 10/11 for an authentication/authorization
failure, 20 for "not found", 21 for a conflict, 30 for a rejected
request, 40/41/50 for server/network/rate-limit failures, 1
otherwise — always, not just when output is json/jsonl, so a shell script
can branch on $? without parsing any output at all.
hint/request_id and a few finer-grained categories are still to come
(PSD-9883).
Profiles are stored under the OS config directory.
On macOS, the default profile path is usually:
~/Library/Application Support/perfectscale-cli/profiles/default.json
Storage behavior:
- profile directory permissions:
0700 - profile file permissions:
0600 auth logoutdeletes the selected local profile
brew install doitintl/tap/pscliTo upgrade to the latest release:
brew upgrade pscliscoop bucket add pscli https://github.com/doitintl/perfectscale-cli
scoop install pscliTo upgrade to the latest release:
scoop update pscliDownload the package for your architecture from Releases and install it directly:
sudo dpkg -i pscli_<version>_linux_amd64.deb # Debian/Ubuntu
sudo rpm -Uvh pscli_<version>_linux_amd64.rpm # Fedora/RHELThis is a direct package install, not a hosted apt/yum repository — there's no
apt install psclioradd-apt-repositorystep.
Download the archive for your platform from
Releases and
extract the pscli binary onto your PATH.
Requirements:
- Go 1.24+
Run directly:
go run . clusters listBuild locally:
make build
./dist/pscli clusters listOr build the binary directly:
go build -o pscli .
./pscli clusters listRegenerate the local public API client:
make openapiRun tests:
go test ./...--profile/-p, --output/-o, --debug/-d, and --public-api-url/-u
work at the top level and on leaf commands. Output modes: table (default,
human-friendly), json (one document), jsonl (one object per line, for
list commands and automation):
pscli -o jsonl workloads list -c prod-a -s waste -r desc -T 10Short options are consistent across commands:
-pprofile,-ooutput,-upublic API URL,-ddebug-ccluster,-wperiod window,-nnamespace-mworkload name,-tworkload type-ssort,-rorder,-Ttop,-Bbottom-Cmin-cost,-Wmin-waste,-Vworkload view-iid or client-id,-kclient-secret or label key (depends on command)-fexport format,-Fexport file-Smin-severity,-vlabel value,-gnode group name
pscli auth login
pscli auth status
pscli clusters listSee the dedicated sections below for each command group's full flags and examples.
workloads list supports client-side filtering and ranking with:
--cluster,-c--period,-w--namespace,-n--name,-m--type,-t--min-cost,-C--min-waste,-W--sort,-s--order,-r--top,-T--bottom,-B--view,-V
Important:
- the public workloads API is fixed to
30dtoday - the CLI enforces
--period 30d - non-cluster filters are applied client-side after the workload list is fetched
Examples:
pscli workloads list -c prod-a -n kube-system -s waste -r desc
pscli workloads list -c prod-a -m api -t Deployment -C 25 -W 10
pscli workloads list -c prod-a -s cost -r desc -T 20workloads list supports view presets through --view or -V.
Available views:
default- cost, waste, namespace, type, and max-indicator overview
capacity- replica counts and current vs recommended request totals
usage- summed container usage percentiles
policy- optimization policy, resilience, and mute state
risk- risk severity, risk counts, and waste counts
all- the broadest enriched workload view
Special behavior:
- if
--view allis used without explicitly setting--output, the CLI switches tojsonl - this makes the full enriched workload objects easier to consume in pipelines and by agents
Examples:
pscli workloads list -c prod-a -V default
pscli workloads list -c prod-a -V capacity
pscli workloads list -c prod-a -V usage
pscli workloads list -c prod-a -V policy
pscli workloads list -c prod-a -V risk
pscli workloads list -c prod-a -V all
pscli -o json workloads list -c prod-a -V allCluster summary:
pscli workloads summary -c prod-aGroup by namespace:
pscli workloads group-by namespace -c prod-a -s waste -r desc -T 10Group by workload type:
pscli workloads group-by type -c prod-a -s workloads -r descGroup by optimization policy:
pscli workloads group-by optimization-policy -c prod-a -s waste -r descGroup by risk severity:
pscli workloads group-by risk-severity -c prod-a -s workloads -r descGroup by label value:
pscli workloads group-by label -c prod-a -k team -s waste -r descShow one workload:
pscli workloads show -c prod-a -i workload-123
pscli workloads show -c prod-a -m api -n backendExport CSV:
pscli workloads export -c prod-a -F workloads.csv
pscli workloads export -c prod-a -n kube-system -s waste -r desc -T 25List risky workloads:
pscli workloads risky -c prod-a -S 2 -s severity -r desc -T 10Explore workload labels:
pscli workloads labels -c prod-a
pscli workloads labels -c prod-a -k app -s waste -r desc -T 20
pscli workloads labels -c prod-a -v productionList muted workloads:
pscli workloads muted -c prod-a -s expires -r ascList clusters:
pscli clusters listGet cluster details:
pscli clusters get -c prod-aShow carbon emission metrics:
pscli clusters emission -c prod-a -s value -r descNamespaces are derived from workloads.
Examples:
pscli namespaces list -c prod-a
pscli namespaces list -c prod-a -s workloads -r desc
pscli namespaces list -c prod-a -n kube -T 5nodegroups list and nodegroups get wrap the public InfraFit node-groups
endpoint: node count/pod count, cost and idle breakdown, CPU/memory/GPU
utilization percentiles, and instance-type or Karpenter recommendations per
node group.
Examples:
pscli nodegroups list -c prod-a
pscli nodegroups list -c prod-a --autoscaler-type karpenter --has-recommendations
pscli nodegroups list -c prod-a --all -o jsonl
pscli nodegroups list -c prod-a -V gpu
pscli nodegroups get -c prod-a -g clickhouseNotes:
-V/--view:default(cost, CPU/memory, recommendation summary) orgpu(GPU architecture/utilization;-in GPU columns for non-GPU groups).-o json/-o jsonlalways include the full payload regardless of view.--autoscaler-type,--has-recommendations,--include-mutedare server-side filters.recommendationsis a discriminated union (standardorkarpenter).nodegroups listtable output shows a summary (type, count, top instance type — the latter only populated forstandard);nodegroups gettable output additionally lists each recommended change (Karpenter: recommended value + rationale; standard: instance type + cost/savings). Use-o json/-o jsonlfor the full payload, including the current value and Karpenter's rawcurrent_config/recommended_configdiff.-o jsonwraps the list as{"node_groups": [...], "pagination": {...}};-o jsonlemits one node group per line with no cursor.--page-sizeis 1–500 (default 50).--page-tokenconsumes the cursor frompagination.next(wire field:meta.pagination.next).--allauto-paginates until no next cursor remains, capped by--page-cap(default 50), always requesting page size 500 regardless of--page-sizesince the backend recomputes the full set each request.
unevictable list, report, show, and muted wrap the public
unevictable-pods endpoints: pods that autoscalers can't evict, why, and what
it's costing. All four are served from the latest pre-computed snapshot for
the cluster — there's no request-time recompute.
Examples:
pscli unevictable list -c prod-a
pscli unevictable list -c prod-a -n payments --reason pod_disruption_budget
pscli unevictable report -c prod-a -C 5 -s blockedCostHourly -r desc
pscli unevictable show -c prod-a -i a1b2c3d4
pscli unevictable muted -c prod-aNotes:
list/reportshow one row per pod with reason codes concatenated (e.g.pod_disruption_budget,node_selector);reportalso carriesnodeandpriority.showreturns full single-pod detail — remediation (fix summary, risk, confidence, current/recommended spec, unified diff) and sibling pod names — not populated bylist/report.-n,--reason,-g,-C/--min-blocked-costare server-side filters (AND-combined).--reasononly works onlist—report's filter schema doesn't accept it.--mutecontrols muted-finding visibility:exclude(default),include, oronly.-s/--sortonly acceptsblockedCostHourly, the only server-side sort key today.mutedis read-only — mute/dismissal rules can only be managed via the web app or the user API.- A 202 (snapshot processing) or 422 (processing failed) response surfaces as a distinct error, not a generic HTTP failure.
- Pagination flags (
--page-size,--page-token,--all,--page-cap) matchnodegroups list. Snapshot metadata (time, algorithm version, summary counts) appears in the table footer and top-level-o jsonfields, but not in--allmode (it spans multiple snapshot reads).
automation audit-logs lists the actions Perfectscale's automation took in your
clusters. The endpoint is cursor-paginated and returns events from the last 30
days.
Examples:
pscli automation audit-logs
pscli automation audit-logs -c prod-a -c prod-b
pscli automation audit-logs -c prod-a -n kube-system -n default
pscli automation audit-logs --from 2026-04-01T00:00:00Z --to 2026-04-15T00:00:00Z
pscli automation audit-logs --since 24h
pscli automation audit-logs --all -o jsonl
pscli automation audit-logs --page-size 200 --after BASE64CURSORNotes:
--cluster(-c) and--namespace(-n) are repeatable. Cluster values may be UID or name.--fromand--toaccept RFC3339 (UTC).--sinceaccepts a relative duration (24h,7d,30m) and is shorthand for--from now-since.--page-sizeis 1–5000 (default 1000).--after/--beforeconsume cursor tokens from a previous response'spagination.next/pagination.prev.--allauto-paginates forward until the server reportshas_next=false, capped by--page-cap(default 50) as a safety net.--executionfilters client-side to one ofregular-eviction,inplace-resize, orcleanup.
pscli commands -o json
pscli commands -o jsonl
pscli commandsWalks the live command tree (no API call) and prints every visible command
with its flags and short-flag aliases. -o json emits one document
{version, cli_version, commands}; -o jsonl emits one command object per
line; table (default) prints PATH and USAGE.
Each command has path (for example ["workloads","list"]), usage,
description, runnable, and flags. Each flag has name, aliases,
type, usage, default, env_vars, required, and runtime (true
for the global profile/output/debug/public-api-url flags copied onto every
command).
pscli updateChecks the latest GitHub release against the running version and prints how
to update, based on the detected install method: brew upgrade pscli,
scoop update pscli, a download+install one-liner for dpkg/rpm, or a link to
the releases page otherwise. Doesn't install anything itself.
-o json/-o jsonl print {"current", "latest", "update_available", "instruction"} instead of the plain-text message.
pscli --version/-v prints the same plain-text status too (always plain
text, regardless of -o), skipped silently on a lookup failure.
pscli skill cursor
pscli skill --allCopies the skill compiled into the binary into that agent's user-level
skills directory (claude/cursor/gemini/kiro →
~/.<agent>/skills/perfectscale/, opencode →
~/.config/opencode/skills/perfectscale/, codex →
~/.agents/skills/perfectscale/). --all covers every agent whose config
dir already exists; --force overwrites local edits after backing them up.
pscli completion bash
pscli completion zsh
pscli completion fish
pscli completion powershellPrints a completion script for the given shell to stdout. bash/zsh/
powershell complete dynamically by shelling back into pscli; fish is
generated from the full command tree.
Install:
# bash
echo 'eval "$(pscli completion bash)"' >> ~/.bashrc
# zsh
echo 'eval "$(pscli completion zsh)"' >> ~/.zshrc
# fish
pscli completion fish > ~/.config/fish/completions/pscli.fish# powershell — add to $PROFILE
Invoke-Expression (& pscli completion powershell | Out-String)CI (build.yml) runs tests on every
push/pull_request — it never creates releases.
Releases are manual: Actions tab → "Release" → "Run workflow", with a bump
input (patch/minor/major, default patch).
release.yml cuts the version tag, then
goreleaser (.goreleaser.yaml)
cross-builds macOS/Windows/Linux (amd64/arm64) and publishes:
- GitHub Release assets: archives, checksums,
.deb/.rpmpackages - Homebrew formula →
doitintl/homebrew-tap(needs theHOMEBREW_TAP_APP_CLIENT_ID/HOMEBREW_TAP_APP_PRIVATE_KEYsecrets; skipped gracefully until they're set) - Scoop manifest → this repo's own
bucket/
Current asset names:
pscli-darwin-arm64.tar.gzpscli-darwin-amd64.tar.gzpscli-windows-amd64.zippscli-windows-arm64.zippscli-linux-amd64.tar.gzpscli-linux-arm64.tar.gzpscli_<version>_linux_amd64.deb/.rpmpscli_<version>_linux_arm64.deb/.rpm
Each release archive contains a pscli binary, or pscli.exe on Windows.
checksums.txt is published alongside them. The goreleaser config lives in
.goreleaser.yaml. The Perfectscale coding-agent skill
is compiled into the binary; install it with pscli skill <agent>.
The local spec is public-api.yaml; it generates
internal/publicapi/client.gen.go via
make openapi. Don't hand-edit the generated client — update the spec and
regenerate. internal/api/client.go is the
handwritten adapter on top: auth headers, response validation, and mapping
into CLI types.
- only service-token auth is supported
- only the public API is supported
- workloads are fixed to a
30dperiod because the public endpoint is fixed-window - namespace and many workload filters are client-side
- CSV is the only export format in v1