# pds CLI

`pds` is the scriptable Prompt Driven Studio control plane for local operators,
CI jobs, and AI agents. It talks to the webapp agent API; it does not run
generation, upload, or provider code directly.

> **New to GVS?** Start with the public
> [GVS onboarding and CLI guide](https://video.promptdriven.ai/agents/cli).
> You do **not** need this CLI to make videos — the Studio at
> <https://video.promptdriven.ai/studio> is the front door. The CLI is an
> optional power-user path. Its device login needs API egress from the CLI
> host. The approving browser may be on another machine and needs ordinary
> access to the verification URL, GVS callback, PDD Cloud/Firebase, and the
> configured Google or GitHub sign-in provider. A sandboxed AI agent (e.g. a
> claude.ai/code cloud session) may block the host and report a "proxy refusing connections"
> error. That is a network-environment diagnosis, not proof of a GVS outage.

## Install From npm

```bash
npm install -g @promptdriven/pds
pds --version
```

Node.js 20 or newer is required.

## Local Development Pack

```bash
cd cli/pds
npm run pack:check
packed_tarball="$(npm pack --json | node -e "const fs = require('node:fs'); const pack = JSON.parse(fs.readFileSync(0, 'utf8')); process.stdout.write(pack[0].filename);")"
npm install -g "./${packed_tarball}"
pds --version
```

Use the local pack path only when testing unpublished changes from a checkout.

## Auth Setup

Use a profile for day-to-day local use:

```bash
pds --api-url http://localhost:3000 --profile local auth login
pds --profile local auth status --json
```

On a headless host, or when approval should happen in a browser on another
machine, add `--no-browser` to `pds ... auth login`. The CLI prints the approval
URL and device code, keeps polling the same login attempt, and saves the selected
profile after approval. `PDS_NO_BROWSER=1` provides the same suppression for
logins launched from that environment. No graphical opener such as `xdg-open`
is required for either manual path.

To install an externally minted raw PDS token into a saved profile, pipe it on
stdin. The CLI verifies the candidate token with `auth status` before writing it
unless `--no-verify` is set, and replacing an existing profile credential
requires `--overwrite`:

```bash
printf '%s\n' "$RAW_PDS_TOKEN" | \
  pds auth login \
    --with-token \
    --token-stdin \
    --profile wildcard-validation \
    --api-url https://video.promptdriven.ai \
    --project pdd-v0-0-295-release \
    --overwrite \
    --json
```

`--project` updates the local profile default only. It does not expand the
server-side `projectIds` allowlist on the token; use `pds auth status --json`
after import to confirm the token id, scopes, and project allowlist.

To change only saved connection metadata without replacing the credential:

```bash
pds auth profile update \
  --profile wildcard-validation \
  --api-url https://video.promptdriven.ai \
  --project pdd-v0-0-295-release \
  --json
```

For CI, create a least-privilege scoped token from an owner account.
Include `project:create` if the token must bootstrap new projects
(e.g. for `pds release-video create` or `pds projects create`):

```bash
pds auth token create \
  --label release-bot \
  --project release-project-id \
  --scopes project:create,project:read,project:write,pipeline:run,artifact:read,distribution:package,distribution:publish \
  --expires-in 30d \
  --json
pds auth token list --json
ROTATE_KEY="token-rotate:existing-token-id:2026-07-04"
ROTATE_EXPIRES_AT="2026-08-03T00:00:00.000Z"
pds auth token rotate \
  --token-id existing-token-id \
  --project recovered-project-id \
  --scopes project:read,pipeline:run,artifact:read \
  --expires-at "$ROTATE_EXPIRES_AT" \
  --idempotency-key "$ROTATE_KEY" \
  --json
```

A token with `project:create` can create new projects that were not listed at
mint time. Store only the returned-once `rawToken` in the CI secret store, then pass
it as `PDS_TOKEN`. Token rotation revokes the previous token and returns the
replacement `rawToken`; reuse the same `--idempotency-key` to recover an exact
dropped retry from the original credential. For retry-safe rotations, use
--expires-at, not --expires-in, when rotating with a stable
`--idempotency-key`, because retries must send the same absolute request body.
For human credentials, human retries are bound to auth source and UID. PDS agent-token retries are bound to the
original token credential, so a different agent token for the same user cannot
replay the replacement `rawToken`. YouTube OAuth credentials stay server-side
and must not be stored in CLI config.

For operator tokens that should follow the user's current memberships, use
actor-scoped project access instead of enumerating every project:

```bash
pds auth token create \
  --label operator-validation \
  --all-my-projects \
  --scopes project:read,pipeline:run,artifact:read \
  --expires-in 1d \
  --json
```

Actor-scoped tokens store `projectIds: { "kind": "actor_projects" }` and
`projectAccess: "actor_projects"`. They are not global wildcard tokens:
each request resolves the token subject's current project membership and then
applies the operation's required role. Removing membership denies the next
request; granting membership to a new project allows the same token to use that
project if scopes and role are sufficient. The CLI defaults actor-scoped token
creation to `--expires-in 1d`; the server rejects actor-scoped expiries beyond
7 days.

Use global wildcard project access only for controlled all-project admin
operator tokens. Cloud wildcard issuance requires Firebase admin custom claim
access on a direct interactive admin auth flow. PDS agent-token profiles cannot
mint cloud wildcard tokens, and device login does not approve wildcard sessions
until a scoped consent preview exists. After an admin flow returns a wildcard
`rawToken`, install it with `pds auth login --with-token --token-stdin`.
Release automation should normally use a project-scoped token with
`project:create`, not wildcard project access.

## Release Video

```bash
PDS_API_URL=https://studio.example.com \
PDS_TOKEN="$PDS_RELEASE_TOKEN" \
pds release-video create \
  --project-name release-v1.8.0 \
  --script release_video_script.md \
  --release-notes CHANGELOG.md \
  --changelog CHANGELOG.full.md \
  --repo-url https://github.com/promptdriven/studio \
  --repo-name promptdriven/studio \
  --git-sha "$GITHUB_SHA" \
  --release-tag v1.8.0 \
  --preset release-notes \
  --target publish \
  --platform youtube \
  --privacy unlisted \
  --idempotency-key "release:v1.8.0:$GITHUB_SHA" \
  --wait \
  --json
```

Add `--dry-run` to inspect the release workflow without starting server-side
generation or publish work.

### GitHub Release automation

Use `pds github-release-video create` to build the request directly from a
GitHub Release and optionally update the release notes with the published
YouTube URL:

```bash
PDS_API_URL=https://studio.example.com \
PDS_TOKEN="$PDS_RELEASE_TOKEN" \
GH_TOKEN="$GITHUB_TOKEN" \
pds github-release-video create \
  --repo promptdriven/studio \
  --tag v1.8.0 \
  --update-release-notes \
  --target publish \
  --platform youtube \
  --privacy unlisted \
  --idempotency-key "github-release:promptdriven/studio:v1.8.0" \
  --json
```

Minimal GitHub Actions workflow:

```yaml
name: Release video

on:
  release:
    types: [published]

permissions:
  contents: write

jobs:
  release-video:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm install -g @promptdriven/pds
      - run: |
          pds github-release-video create \
            --repo "$GITHUB_REPOSITORY" \
            --tag "${{ github.event.release.tag_name }}" \
            --update-release-notes \
            --target publish \
            --platform youtube \
            --privacy unlisted \
            --idempotency-key "github-release:${GITHUB_REPOSITORY}:${{ github.event.release.tag_name }}" \
            --json
        env:
          GH_TOKEN: ${{ github.token }}
          PDS_API_URL: https://studio.example.com
          PDS_TOKEN: ${{ secrets.PDS_RELEASE_TOKEN }}
```

The PDS token should be scoped to `project:create`, `project:read`,
`project:write`, `pipeline:run`, `artifact:read`, `distribution:package`, and
`distribution:publish`.

### Metadata conflict recovery

Run `pds release-video create --help` for the full create flag surface.
`--metadata-conflict fail|use-existing|replace` controls initialized
selected-project recovery when `outputs/agent/release-video/metadata.json`
differs from the request. `fail` is the default. Use
`--metadata-conflict use-existing` only when preflight confirms the stored
sidecar is the release metadata you want to keep. Use
`--metadata-conflict replace` with `--force-regenerate` only after preflight
confirms the selected project is safe to reuse.

### Fixed-project recovery

Use `--bootstrap-selected-project` only when a pre-authorized fixed project is
safe to initialize for the requested release. The API service must allow the
missing-sidecar bootstrap by listing the exact project id in
`PDS_RELEASE_VIDEO_BOOTSTRAP_PROJECT_IDS` (comma or whitespace separated, no
wildcards), or by setting trusted project metadata
`releaseVideo.bootstrapAllowed: true`. Run preflight first, then create with
`--force-regenerate` so stale artifacts cannot be reused:

```bash
pds release-video preflight \
  --project pdd-release-bootstrap \
  --bootstrap-selected-project \
  --script release_video_script.md \
  --release-tag v0.0.279 \
  --target publish \
  --platform youtube \
  --privacy unlisted \
  --json

pds release-video create \
  --project pdd-release-bootstrap \
  --bootstrap-selected-project \
  --script release_video_script.md \
  --release-tag v0.0.279 \
  --target publish \
  --platform youtube \
  --privacy unlisted \
  --force-regenerate \
  --idempotency-key "release:v0.0.279:bootstrap" \
  --wait \
  --json
```

Check `releaseMetadataSidecar.source`: `existing` means the selected project
already matched the requested release, while `bootstrapped` means recovery
initialized its metadata and script for this run.

## Scripts

```bash
pds script set --project release-v1.8.0 --file release_video_script.md --idempotency-key release:v1.8.0:script --json
pds script set --project release-v1.8.0 --file tts_script.md --target tts --idempotency-key release:v1.8.0:tts-script --json
```

`script set` writes the main script by default. The only alternate target is
`tts`; unsupported targets fail locally before the API request.

For direct uploads,
`pds distribution publish --privacy unlisted --idempotency-key <key> --wait`
refreshes the Stage 14 package before upload. Add `--no-generate` to require an
existing ready package, or `--dry-run` to plan without packaging or uploading.
The JSON response exposes redacted advisory details in
`packageStatus.validationWarnings`; these do not block publishing unless a
corresponding condition is also present in `packageStatus.blockingIssues`.

Use this command to attach a local PNG or JPEG thumbnail to the selected
distribution candidate:

```bash
pds distribution thumbnails upload --file thumbnail.png --confirm-rights --idempotency-key <key>
```

Add `--replace-live` only when the already-published YouTube video should
receive the uploaded thumbnail immediately.

## Pipeline

`pds pipeline run` supports scoped reruns for server-side stage bodies:

```bash
pds pipeline run \
  --project brand-demo \
  --to render \
  --claude-model glm-5.2 \
  --sections segment_update \
  --segments segment-1,segment-2 \
  --clips clip-1 \
  --dry-run \
  --json
```

`--claude-model` accepts `glm-5.2`, `glm-5.2[1m]`, `opus-4.8`, or
`claude-opus-4-8`; the server normalizes and validates the value.
`--sections`, `--files`, `--segments`, and `--clips` accept comma-separated
values and may be repeated. Scope and model flags are intentionally limited to
`pipeline run`; the current server `pipeline plan` endpoint returns unscoped
stage bodies and would otherwise give a misleading plan.

Storyboard execution overrides are available on both `pipeline run` and
`pipeline plan`:

```bash
pds pipeline plan \
  --project brand-demo \
  --to render \
  --storyboard-mode skip \
  --json

pds pipeline run \
  --project brand-demo \
  --to render \
  --storyboard-mode review \
  --storyboard-model google-vertex:gemini-3.1-flash-image \
  --idempotency-key <key> \
  --wait \
  --json
```

`--storyboard-mode` accepts only `skip` or `review`.
`--storyboard-model` requires a canonical `provider:model` key and implies
review mode. Servers that do not expose Storyboard review execution reject
review/model overrides before starting work; explicit `skip` retains the
default pipeline behavior.

Review availability is server-owned: deployments may keep it `off`, expose it
only to exact internal project allowlists, or enable general eligible projects.
Project configuration and CLI overrides do not bypass that policy.

When `pipeline run --wait` reaches a trusted Storyboard review checkpoint, it
returns a successful handoff with `targetReached: false` instead of claiming
the requested target completed. Review the same-origin URL in that result,
then run the exact derived approval command:

```bash
pds storyboard approve \
  --run <run-id> \
  --fingerprint sha256:<64-lowercase-hex> \
  --json
```

The CLI reads the run first and derives the project and continuation generation
from its trusted review evidence. Those values cannot be supplied directly.

To revalidate exactly one preserved Veo artifact after structured-analysis
normalization exhaustion, use the capability-gated recovery surface:

```bash
pds pipeline run \
  --project brand-demo \
  --stage veo \
  --mode stage-only \
  --clips clip-1 \
  --veo-validation-only \
  --idempotency-key "brand-demo:veo:validate:clip-1" \
  --wait \
  --json
```

The CLI submits this request only when the server advertises exact numeric
capability `veoValidationOnlyRecovery: 1`. The older `--validation-only`
spelling is a compatibility alias with the same fail-closed preflight and
request semantics.

## Reference Libraries

```bash
pds reference-library list --project brand-demo --json
pds reference-library create \
  --project brand-demo \
  --label "Brand Demo Library" \
  --slug brand-demo \
  --library-id lib_user_brand_demo \
  --idempotency-key "brand-demo:reference-library:create" \
  --json
pds reference-library items list --project brand-demo --library-id lib_org_brand --json
pds reference-library policy get --project brand-demo --json
pds reference-library policy set \
  --project brand-demo \
  --capture-policy private-draft \
  --default-capture-library-id lib_org_brand \
  --no-auto-select-from-library \
  --idempotency-key "brand-demo:reference-library:policy-enable" \
  --json
pds reference-library policy set \
  --project brand-demo \
  --capture-policy off \
  --idempotency-key "brand-demo:reference-library:policy-disable" \
  --json
pds reference-library bindings list --project brand-demo --json
pds reference-library bindings attach \
  --project brand-demo \
  --library-id lib_org_brand \
  --item-id primary_logo \
  --reference-id brand_logo_primary \
  --idempotency-key "brand-demo:reference-library:attach-logo" \
  --json
pds reference-library bindings update \
  --project brand-demo \
  --binding-id binding_brand_logo_primary \
  --to-version-id v2 \
  --dry-run \
  --json
pds reference-library bindings detach \
  --project brand-demo \
  --binding-id binding_brand_logo_primary \
  --idempotency-key "brand-demo:reference-library:detach-logo" \
  --json
pds reference-library grants list \
  --project brand-demo \
  --library-id lib_user_brand_demo \
  --json
pds reference-library grants add \
  --project brand-demo \
  --library-id lib_user_brand_demo \
  --user-id user_123 \
  --permission can-use \
  --expires-at "2026-12-31T00:00:00.000Z" \
  --idempotency-key "brand-demo:reference-library:grant-user-123" \
  --json
pds reference-library grants add \
  --project brand-demo \
  --library-id lib_user_brand_demo \
  --user-id user_456 \
  --permission admin \
  --idempotency-key "brand-demo:reference-library:grant-user-456" \
  --json
pds reference-library grants revoke \
  --project brand-demo \
  --library-id lib_user_brand_demo \
  --grant-id grant_123 \
  --idempotency-key "brand-demo:reference-library:revoke-grant-123" \
  --json
```

Use `--reference-id` for Stage 3 Reference Catalog targets, or `--target-json`
for other project artifacts.

Reference Library admin commands are guarded by agent version negotiation. The
server must return `features.referenceLibraryAdmin: true` from
`/api/agent/version`; otherwise the CLI stops before calling admin endpoints.
Grant presets are intentionally limited: `can-use` maps to `library:view` and
`library:use`, while `admin` maps to `library:view`, `library:use`, and
`library:admin`. The CLI does not expose `library:copy`.

Server-side idempotency is atomic and non-dry-run mutating release commands
plus agent token rotation require `--idempotency-key`. Agent token records are
durable on the service.

Use `--json` for one final JSON object and `jobs watch --jsonl` for one event
object per line.

## Checks

```bash
npm --prefix cli/pds test
npm --prefix cli/pds run smoke:pack
```

The root shortcut is `npm run test:pds-cli`.
