Scripting and Automation

PCLI2 is designed to work well in shell scripts, cron jobs, and CI/CD pipelines. This page collects the features that matter when no human is watching the terminal.

Table of Contents

Machine-Friendly Output

Colors, spinners, and progress bars are shown only when the output is a terminal. When you pipe or redirect output, you get clean text automatically:

# Clean JSON, no ANSI escape codes
pcli2 asset list --folder-path "/Home/Models/" --format json | jq '.[].name'

# CSV with headers for spreadsheets
pcli2 asset list --folder-path "/Home/Models/" --format csv --headers > assets.csv

To disable colors explicitly, use the --no-color flag or set the NO_COLOR (or PCLI2_NO_COLOR) environment variable.

The same rules apply to diagnostics on stderr: warnings and --verbose logs captured with 2> warnings.log are plain text with no ANSI escape codes, so they can be grepped and parsed directly.

Safe CSV for Spreadsheets

A CSV cell that starts with =, +, - or @ is evaluated as a formula by Excel, LibreOffice and Google Sheets when the file is opened, and asset names and metadata values come from whoever uploaded them. With --safe-csv (or PCLI2_SAFE_CSV=1) such cells are written with a leading single quote, which spreadsheets show as plain text. Values that are numbers, such as -5, are left alone. The default is off because the quote is visible to every other consumer of the file.

pcli2 asset list --folder-path "/Home/Parts" --format csv --headers --safe-csv > parts.csv

Excel workbooks written with --format xls are not affected: their cells are stored as strings and are never evaluated.

Machine-Readable Errors

With --error-format json (or PCLI2_ERROR_FORMAT=json) everything pcli2 writes to stderr is one JSON object per line: errors, their hints, warnings, --verbose log lines and the --stats summary. The last object of a failed run carries the exit code and its class:

$ pcli2 --error-format json asset delete --path /Home/Parts/nope.stl
{"level":"ERROR","code":67,"kind":"not_found","message":"API error: Path not found: /Home/Parts/nope.stl"}
$ echo $?
67
FieldMeaning
levelERROR, WARN, INFO or DEBUG, matching the log lines
codeThe process exit code, on the final error object
kindThe failure class: usage, data, no_input, not_found, unavailable, temp_fail, software, os, config, auth, network, api
messageThe same text the human-readable error would show
hintWhat to do about it, when pcli2 knows
http_statusThe HTTP status behind an API error, when there is one
stepsRemediation steps, on errors that list them

Progress bars and the upload/download statistics reports are not JSON; leave --progress off in scripts that parse stderr.

# Read the exit code and message of a failed run
if ! out=$(pcli2 --error-format json asset list --folder-path "/Nope" 2>&1 >/dev/null); then
  echo "$out" | tail -n 1 | jq -r '"\(.kind): \(.message)"'
fi

Skipping Prompts

Destructive commands ask for confirmation when run interactively. In scripts, pass --yes:

pcli2 folder delete --folder-path "/Home/Scratch/" --force --yes
pcli2 cache clear --yes

A prompt that cannot be shown is refused rather than answered for you: when stdin is not a terminal, or --no-input (or PCLI2_NO_INPUT=1) is set, a command that would have to ask exits 64 and says which flag to pass instead. tenant use and env use without --name fail the same way instead of showing a menu nobody can answer. Set PCLI2_NO_INPUT=1 in CI so a forgotten --yes fails fast rather than hanging on a prompt.

Authentication credentials can be passed as flags for non-interactive use:

pcli2 auth login --client-id "$PHYSNA_CLIENT_ID" --client-secret "$PHYSNA_CLIENT_SECRET"

Dry Run Mode

Preview destructive or bulk operations without changing anything on the server. Supported by asset delete, folder delete, asset create, asset create-batch, and folder upload:

# List exactly which files a batch upload would send, and where
pcli2 asset create-batch --input "build/*.stl" --folder-path "/Home/CI Builds/" --dry-run

# Confirm what a forced folder delete would remove
pcli2 folder delete --folder-path "/Home/Old Projects/" --force --dry-run

Exit Codes

PCLI2 uses distinct exit codes (following BSD sysexits.h conventions where possible) so scripts can react to specific failure classes:

CodeMeaning
0Success
64Command line usage error
65Data format error
66Cannot open input file
67Resource not found
69Temporary failure
70Internal software error
71Operating system error
78Configuration error
100Authentication error
101Network communication error
102Remote API error

A usage error rejected by the argument parser also exits 64. Batch commands that finished with some items failed, and folder matches whose report would be incomplete, exit 69.

pcli2 asset get --path "/Home/Models/part.stl" --format json
case $? in
  0)   echo "found" ;;
  100) pcli2 auth login ;;
  101) echo "network problem, try again later" ;;
  *)   echo "failed" ;;
esac

Verbosity Control

The global --quiet flag limits diagnostics to errors; --verbose (-v) enables debug-level logging. Both work on every command and take precedence over the PCLI2_LOG_LEVEL and RUST_LOG environment variables:

pcli2 --quiet asset create-batch --input "build/*.stl" --folder-path "/Home/CI Builds/"
PCLI2_LOG_LEVEL=trace pcli2 folder list

Automatic Retries

Transient failures (network timeouts, connection errors, and HTTP 408/429/502/503/504 responses) are retried automatically with exponential backoff, honoring the server's Retry-After header. The default is 2 retries; tune it with PCLI2_MAX_RETRIES (0 disables retries):

PCLI2_MAX_RETRIES=5 pcli2 folder download --folder-path "/Home/Models/" --output ./downloads

The request timeout defaults to 30 minutes (large model files take that long to transfer). Lower it with PCLI2_TIMEOUT (seconds) if you prefer fast failures over patience:

PCLI2_TIMEOUT=120 pcli2 asset list --folder-path "/Home/Models/"

Note that timeouts abort-and-retry only read requests (GETs); a timed-out write is never retried automatically because the server may have already processed it.

Resuming Interrupted Runs

Long runs in a script should be written so that a retry does not redo finished work:

# Downloads skip files already on disk
pcli2 folder download --folder-path "/Home/Parts" --output ./parts --resume

# Uploads skip files whose name is already in the folder
pcli2 asset create-batch --input "parts/*.stl" --folder-path "/Home/Parts" --skip-existing

# Folder matches record each completed search; the same command continues the run
pcli2 folder geometric-match --folder-path "/Home/Parts" --recursive \
  --checkpoint parts.jsonl --format csv --headers > parts.csv

The checkpoint file is removed when the report is written, so a loop that retries until the command exits 0 needs no cleanup of its own.

Request Statistics

Add --stats to any command to get one line on stderr at exit with the number of API requests made, how many were retried, how many token renewals happened, and the elapsed time. It is the quickest way to see whether a batch is doing more work than it should:

pcli2 --stats asset metadata create-batch --input metadata.csv
# 📊 5,102 API request(s), 2 retried, 1 token renewal(s) in 3m41s

Checking the Setup

pcli2 doctor prints the local state in one screen (binary and PATH, configuration, environment, credentials, token expiry, tenant, caches, API and auth-server reachability, update state) and exits non-zero when something is wrong: 78 for a local problem, 68 when a server cannot be reached. --format json makes it machine-readable.

Update Notifications

In interactive terminal sessions, PCLI2 prints a one-line hint on stderr when a newer release is available (checked at most once per day). The check never runs in CI (detected via the CI environment variable) or when output is redirected. To opt out entirely:

export PCLI2_NO_UPDATE_CHECK=1

CI/CD Example

A GitHub Actions job that uploads build artifacts to Physna:

jobs:
  upload-models:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install pcli2
        run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/jchultarsky101/pcli2/releases/latest/download/pcli2-installer.sh | sh
      - name: Authenticate
        run: pcli2 auth login --client-id "${{ secrets.PHYSNA_CLIENT_ID }}" --client-secret "${{ secrets.PHYSNA_CLIENT_SECRET }}"
      - name: Upload models
        run: |
          pcli2 tenant use --name my-tenant
          pcli2 asset create-batch --input "build/*.stl" \
            --folder-path "/Home/CI Builds/" --quiet --format json