Packdog CLI
Framework-agnostic CLI for shipping, promoting, and managing packs against the Packdog API at api.packdog.dev/v1.
Setup
1. Sign in
Run:
packdog loginThis opens your browser to a sign-in page and waits. Sign in there — the CLI catches the result on a local loopback redirect, saves your session to ~/.config/packdog/config.json, and you're signed in from any directory afterwards. The session holds short-lived tokens that the CLI refreshes for you transparently; you stay signed in until you packdog logout.
packdog login signs you in as a human — you act with the role (admin or member) you hold in your organization. There is one session slot; running packdog login again replaces it.
CI / automation — set a token via env: browser sign-in is for humans. For CI, an admin generates a ci_* CI token in the panel under CI tokens, you paste it into your secrets manager as PACKDOG_TOKEN, and the CLI reads it automatically:
PACKDOG_TOKEN=ci_... packdog deploy --channel=test --dir=distCredential resolution order: PACKDOG_TOKEN environment variable → ~/.config/packdog/config.json (the packdog login session). The CLI reads real environment variables only — it does not scan for .env files. If you keep the token in a file, load it yourself (your shell, direnv, dotenv-cli, or your framework) so the CLI never has to guess your env-file layout.
2. Tell the CLI which package — three ways
Every package command needs to know which package. It's resolved in this order:
-p/--package=<name|id>flag — a package name, aproject/namehandle, or apkg_id. Works from anywhere with no config file.PACKDOG_PACKAGE_IDenvironment variable — handy for CI and scripts.packdog.jsonin the current directory — the convenience for working inside a pack's project.
Names vs ids. A package name is a handle — unique within its project — so you can type -p checkout-widget instead of an id; the CLI resolves it to an id for you. If the same name exists in more than one of your projects, qualify it with the project/name form — -p marketing/checkout-widget — or just use the pkg_ id. Use names when you're typing a command; use the id for stored references — PACKDOG_PACKAGE_ID and packdog.json hold the stable id, immune to a rename elsewhere.
packdog.json is optional — never required. It is a pure identity file: it holds exactly one field, the stable id, and answers one question, "which package is this directory?"
{
"packageId": "pkg_…"
}Create it with packdog link <package> (a name or a pkg_ id) or write it by hand. Nothing about how you run commands lives here — channel and directory are flags (see packdog deploy below), so the same command means the same thing in every directory. If you run the same invocation often, put it in a package.json script.
Auth tiers in one paragraph
A ci_* CI token authorises against the CI token endpoint family (/v1/packages/{id}/...) — scoped to the packages in the projects the token is granted on (multi-project grants, each carrying its own canPublishProtected flag). A signed-in user (via packdog login) authorises against the self-service family (/v1/me/...): an admin gets full power over packages the organization owns plus lifecycle ops (manage CI tokens, create/delete packages, rotate the load key), a member is scoped to the packages an admin has granted them. The CLI command set is unified: every command works the same regardless of which credential you loaded. Commands that need an admin (like ci-tokens list, keys rotate) refuse a ci_* token — or a member — with a clear message naming the command.
Commands
packdog login
Sign in through the browser (OAuth 2.0 Authorization Code + PKCE with a loopback redirect). Opens your browser, waits while you sign in, and saves the resulting session to ~/.config/packdog/config.json.
packdog login
# Opening your browser to sign in...
#
# Waiting for sign-in to complete...
# ✓ Signed in as alice@acme.com (Acme Corp, admin).
# Session saved to ~/.config/packdog/config.jsonThe redirect lands on a local loopback port, so packdog login needs a browser on the same machine. On a headless box (CI, an SSH session with no local browser) use a ci_* CI token via PACKDOG_TOKEN instead — see Setup and CI usage.
The CLI does not accept a positional token argument.
packdog logout
Clear the saved session.
packdog logoutpackdog whoami
Show who you are signed in as. The output shape mirrors your credential — a ci_* CI token shows its packages, a signed-in user shows the organization summary with role and quotas. Add --json for the raw object.
packdog whoami
# CI token: Marketing site CI
# Packages:
# checkout-widget pkg_…
# promo-banner pkg_…
# Or, signed in as a user:
# Signed in: alice@acme.com (admin)
# Organization: Acme Corp
# ID: org_…
# Key: load_a3f8e2c1…
# Packages: 3 / 10
# CI tokens: 2 / 50packdog --version
Show the CLI version.
packdog --version # or: packdog -vpackdog packages
List packages your credential can act on (this is also packdog packages list). For a ci_* CI token this is every package in the projects the token is granted on; for a signed-in admin it is the organization's owned packages; for a member it is the packages granted to them. The lifecycle subcommands — create, rename, delete — are admin-only and documented in the Organization-admin section below.
packdog packages
# Packages:
# checkout-widget pkg_…
# promo-banner pkg_…
packdog packages --json # machine-readablepackdog link
Link the current directory to an existing package — writes a one-field packdog.json holding the package's stable id. The argument is a package reference: a name, a project/name handle, or a pkg_ id (a name is resolved for you, the same way -p resolves it). What's written is always the resolved id, so a later rename never breaks the file. The package must already exist (create one with packdog packages create or in the web panel) — link connects to it, the way vercel link / netlify link connect a folder to an existing project.
packdog link <package> # e.g. packdog link checkout-widget (or a pkg_ id)packdog inspect
Show a package's details — id, created time, source repo (if set), version count, and each channel with the version it currently points at. The package comes from -p/--package, PACKDOG_PACKAGE_ID, or packdog.json. Add --json to emit the same data as a machine-readable object for scripting.
packdog inspect
packdog inspect --json # machine-readablepackdog open
Open the package's page in the web panel (app.packdog.dev) in your browser. The URL is always printed too, so a headless session can copy it; set PACKDOG_NO_OPEN=1 to skip the browser launch.
packdog open # package from packdog.json / -p / envpackdog deploy
Upload all files from a directory as a new version and publish that version to a channel, in one step. Any non-empty directory works — there's no required entry filename. Packdog serves the bundle from a baseUri, and you load whatever entry it has (index.js, main.js, or an index.html for an <iframe>).
--channel=<name> and --dir=<path> are both required. There is no default channel and no default directory, and nothing is read from packdog.json — so packdog deploy ... means the same thing in every directory. Relative --dir paths resolve from the current directory.
packdog deploy --channel=test --dir=dist # upload dist/ and publish to test
packdog deploy --channel=test --dir=build # upload build/ instead
packdog deploy --package=<id> --channel=test --dir=dist # no packdog.json neededIf you run the same invocation often, pin it in a package.json script — that is the natural home for build-and-ship commands, and it stays visible:
{ "scripts": { "ship": "vite build && packdog deploy --dir=dist --channel=test" } }The publish line shows the channel transition — Published to prod: ver_old → ver_new — so you always see what you replaced (and what rollback would return to). A first publish shows just the new id.
Hidden files are never uploaded. Any file or directory whose name starts with . (.env, .git/, .npmrc, .DS_Store, …) is skipped, at any depth. Uploaded files are served from a public CDN, so a --dir accidentally pointed at a project root must not publish secrets or VCS metadata. The skip is reported in the output, never silent. If your build output genuinely needs a dot-named file, rename it in your build step. (Symlinks are also neither followed nor uploaded.)
Optional source tags. Add --label=<text> (a human version name, e.g. v1.2.3) and/or --commit=<sha> (the exact source commit) to record where a version came from. Both are frozen onto the version and shown wherever it appears — in packdog versions, packdog history, and the web panel (where the commit links to your repository if you've set one). They are explicit — the CLI never runs git for you. In CI, wire them to what the platform hands you:
packdog deploy --channel=prod --dir=dist --label=v1.2.3 --commit=$GITHUB_SHAThe target channel must already exist — deploy does not create channels. Publishing to a channel that doesn't exist fails with a 404 ("Channel '...' does not exist — create it first"). A channel is created by an organization admin (packdog channels create, or packdog packages create --channels=...). This means a typo'd channel name is caught immediately instead of silently creating a junk channel.
Publishing to a protected channel: a ci_* CI token needs canPublishProtected = true on its grant for the package's project, a member needs canPublishProtected on either their per-package grant OR per-project grant (UNION). An admin bypasses this on packages the organization owns.
packdog promote
Point a channel at an existing version — no upload. Requires --to=<channel> plus exactly one source:
--from=<channel>— promote whatever version that channel currently points at--version=<id>— promote a specific version--version=latest— promote the newest uploaded version
packdog promote --to=prod --from=test # promote test's current version to prod
packdog promote --to=test --version=<id> # publish a specific version to test
packdog promote --to=beta --version=latestThere is no default target channel and no implicit version — --to and exactly one source are always required. The --to channel must already exist; promote does not create channels and returns a 404 if the channel is unknown. Same protected-channel rule as deploy.
packdog rollback
Roll back a channel to its previous version. Each call steps one version back. --channel is required.
packdog rollback --channel=prod
# Rolled back prod: ver_new → ver_previousThe output shows the transition — what was live, and what's live now. Same protected-channel rule as deploy.
packdog versions
List the pack's versions (newest first). Each row shows file count, size, upload time, who uploaded it, and which channels are active on it. The uploader is a CI token's name or a signed-in user (operator for operator uploads); versions uploaded before attribution existed show —. Add --json for machine-readable rows (same for packdog channels --json).
The list is paginated — you get the most recent page (with a hint when more exist). --all fetches every version (following the cursor); --limit=N sets the page size. With --json, you get the current page; add --all for the whole list.
packdog versions
# ver_… 1 file 45.2 KB 2026-04-04 10:22 by Marketing site CI [prod, test]
# ver_… 1 file 44.8 KB 2026-03-28 14:05 by —
# … more versions not shown. Use --all to fetch everything, or --limit=N for a bigger page.
packdog versions --all # every version
packdog versions --limit=100 # a bigger pageSubcommands (admin only):
packdog versions cleanup [-p <package>] [--keep=N] # bulk-delete old unprotected versions (default keep 10)
packdog versions delete <versionId> [-p <package>] # delete one version (protected → 409)packdog channels
List all channels and their current versions. A channel that has been created but has nothing published to it yet shows its version as (empty).
packdog channels
# prod ver_7Hq2Np4mR8Kt...
# stage (empty)Subcommands (admin only). The channel is positional; the package comes from -p/--package (a name, project/name, or pkg_ id) or packdog.json — just like every other command:
packdog channels create prod [--protected] # create an open or protected channel
packdog channels delete prod # delete it (history gone, versions kept)
packdog channels protect prod # mark protected (production gate)
packdog channels unprotect prod # reopen the channel
packdog channels lock prod # lock — publish/rollback return 409 until unlocked
packdog channels unlock prod # remove the lock
packdog channels create prod -p checkout-widget # name the package explicitlyThe channel name follows the slug rule (lowercase letters, digits, hyphens; 1–63 chars; must start with a letter or digit). rollback and cleanup are reserved. A package can hold at most 10 channels. A newly-created channel starts empty — it shows as (empty) in packdog channels until a version is published to it.
protect vs lock — orthogonal concepts:
protectedis a permission rule — protected channels can only be published / rolled back by callers with the relevant protected flag (a CI token'scanPublishProtectedon its grant for the package's project, or amember-role human'scanPublishProtectedon either their per-package OR per-project grant — UNION). Operator + admin/owner-role humans always qualify. It is a who-can-do-this gate.lockis a total lockout — locked channels refuse publish / rollback from every caller, including operator. To act, unlock first. It is a when-publishes-are-accepted gate.
A channel can be neither, either, or both at once. lock is the right tool for demo windows, deploy stabilisation, and incident holds where the answer should be "no publishes, period."
packdog history
Show a channel's publish log — every version ever published to that channel, newest first, with the currently-live version marked. --channel is required.
Like versions, the log is paginated — the most recent page by default, --all follows the cursor to fetch every publish, --limit=N sets the page size.
packdog history --channel=prod
# 2026-05-17 10:30:00 ver_7Hq2Np4mR8Kt... ← current
# 2026-05-16 14:02:00 ver_3pLm9WqV2sB...
packdog history --channel=prod --json
packdog history --channel=prod --all # every publishAfter a rollback, ← current sits on an older entry, not the newest publish. The log shows version and time only — not who published. For who-did-what, use packdog audit.
packdog audit
Show the package's audit log — every mutating action (upload, publish, rollback, the channel/version lifecycle, rename, grants), newest first, with the actor that performed it. Paginated: the most recent page by default (--limit sets the page size, capped at 1000), --all follows the cursor to fetch every action. Add --json for the raw entries (the current page; add --all for everything).
packdog audit
# 2026-05-17 10:30:00 channel.publish alice@acme.com → prod {versionId=ver_7Hq2Np4mR8Kt}
# 2026-05-17 10:28:00 version.upload alice@acme.com → ver_7Hq2Np4mR8Kt...
packdog audit --limit=20
packdog audit --all # the full historyFor operator and CI-token actions the actor name is captured when the action happens, so the log stays readable even after the token is removed; for a human actor the panel resolves the user to a real name.
Admin commands (require an admin sign-in)
These need an organization admin. If you run them with a ci_* CI token loaded, or signed in as a member, the CLI refuses with a clear message naming the command — no request is fired.
packdog projects
List the organization's projects, or create one. A project is the namespace packages live in and the boundary CI tokens + members are granted on — packages create requires one, so this completes the zero-to-deploy loop without leaving the terminal. Rename, delete, and grant management live in the panel.
packdog projects
# Projects:
# Marketing site prj_… 3 packages
packdog projects --json # machine-readable
packdog projects create "Storefront"
# Created project Storefront
# ID: prj_…packdog ci-tokens
Manage the ci_* CI tokens your organization has issued. A CI token's name is a free-text label shown in the audit log — name it after the project it's scoped to or the pipeline that uses it (Marketing site CI, nightly-build), not a single package: a token can publish every package in its project.
packdog ci-tokens list
# CI tokens:
# Marketing site CI ci_a3f8e2c1… cit_…
# nightly-build ci_b4d92f0a… cit_…
packdog ci-tokens add "GitHub Actions" --project=marketing-site
# Created CI token GitHub Actions
# ID: cit_…
# Token: ci_freshtoken12345abcdef
# Scope: Marketing site
#
# For additional project grants or Protected publishing, manage from the panel:
# https://app.packdog.dev
# Store the token securely — it will not be shown again.
packdog ci-tokens rename <id> "build-bot"
# Renamed to 'build-bot'.
packdog ci-tokens delete <id> --confirm=build-bot
# CI token 'build-bot' deleted.There is no deactivate / reactivate — the lifecycle is create → use → delete (matches GitHub PATs, Stripe API keys, WorkOS API keys). delete is irreversible and requires --confirm to echo the token's current name.
A token is granted to one project at creation. --project=<name|id> picks it; a single-project org auto-selects its sole project, and a multi-project org without --project gets the list to choose from. A brand-new org with zero projects must create its first project before any CI token can be created (packdog projects create <name>). Finer scoping (additional project grants, Protected publishing) lives in the panel on the project detail page → CI tokens tab.
To give a human access to packages, invite them to your organization in the web panel — they sign in with packdog login. ci_* tokens are for machines.
packdog packages create / rename / delete
Lifecycle ops on packages your organization owns (admin-only). Listing is packdog packages (above).
packdog packages create <name> # create a package (single-project org: auto-selects)
packdog packages create <name> --project=<name|id> # choose the project (required for multi-project orgs)
packdog packages create <name> --channels=stage,test,prod # + create channels in the same step
packdog packages rename <package> <newName> # rename a package (package = name or pkg_ id)
packdog packages delete <package> --confirm=<name> # wipe everything (irreversible); package = name or pkg_ idEvery package belongs to exactly one project. If your organization has a single project, create selects it automatically; if it has several, pass --project=<name|id> (the command lists the available projects if you omit it or name one that doesn't exist). An organization with no projects yet must create one first (in the panel).
Each organization has a package quota. If you're already at the quota, create fails with Package limit reached (N/N). Contact us to raise it.
rename changes the display name; clients resolve packages by the stable pkg_… id, so a rename never breaks anything. The new name must be free within the package's project (else 409) and follows the slug rule (lowercase letters, digits, hyphens; 1–63 chars).
delete wipes every version, channel, and stored file, and is not reversible. To make a fat-finger deletion impossible it requires --confirm echoing the package's current name. Without --confirm, or with a name that doesn't match, the CLI refuses and no request is sent.
packdog usage
Per-package load counts for your organization. Default --days=30, max 90.
packdog usage
packdog usage --days=7
# Usage (last 7 days):
# checkout-widget 12,345 loads
packdog usage --json
# { "period": "last 30 days", "packages": [ { "packageId": "pkg_…", "packageName": "checkout-widget", "loads": 12345 } ] }packdog keys
Manage the organization's load_* load key(s). packdog keys (or packdog keys list) shows your keys — the current one plus any retiring key still valid in a rotation grace window; packdog keys rotate issues a new one; packdog keys revoke <keyId> ends a retiring key's grace window early.
Load keys are retrievable. A load key is a publishable credential — it ships in your front-end code by design — so keys list (and the panel's Settings page) shows the full value, always. There is no "shown once" dance. (A key issued before 2026-06-10 has no stored value and lists as its prefix; rotate to get a viewable one.)
Rotation is zero-downtime by default: the previous key keeps working through a grace window (so already-deployed pages don't break while you roll out the new one), and you revoke it sooner with keys revoke once you've redeployed everywhere. Pass --immediate to revoke the previous key right away — use only if it leaked.
packdog keys
# Load keys:
# load_current1234abcd5678 current
# load_retiring5678efgh1234 retiring — valid until Fri, 06 Jun 2026 11:00:00 GMT (revoke early: packdog keys revoke lk_…)
packdog keys rotate
# New load key generated.
# Key: load_brandnew1234567890
# Prefix: load_brandnew
# Your previous key keeps working until Fri, 06 Jun 2026 11:00:00 GMT.
# Once you've redeployed everywhere, revoke it sooner with 'packdog keys revoke <keyId>'.
# The key stays viewable — 'packdog keys list' or the panel's Settings page.
packdog keys rotate --immediate
# … Your previous key was revoked immediately.
packdog keys revoke lk_abc123
# Retiring load key revoked. It no longer loads packages.After rotating, update the new key in every browser bundle / runtime config that loads packages.
Typical workflow
# See which packages you have access to
packdog packages
# Link the local repo to a package (a name or a pkg_ id)
packdog link <package>
# Build your pack (project-specific)
npm run build
# The test and prod channels must already exist. An org admin creates them once:
# packdog channels create test -p <package>
# packdog channels create prod -p <package>
# Ship the build — upload dist/ and publish to the test channel in one step
packdog deploy --channel=test --dir=dist
# → Uploaded: ver_7Hq2Np4mR8Kt
# → Published to test: ver_7Hq2Np4mR8Kt
# Test on the test channel, then promote that version to prod
packdog promote --to=prod --from=test
# → Published to prod: ver_7Hq2Np4mR8Kt
# Something wrong? Roll back (--channel required)
packdog rollback --channel=prodExit codes
| Code | Meaning |
|---|---|
0 | Success |
1 | Usage error (missing argument, missing directory, no target channel, no package resolved, etc.) |
2 | Client error from the API (4xx other than 429) |
3 | Network error after retries exhausted |
4 | deploy uploaded the version but the publish step failed (the version id is still in the output) |
5 | Server error after retries (5xx) or unparseable response |
Network errors and 5xx/429 responses are retried up to 3 times with exponential backoff before the CLI gives up. Use these codes in CI scripts to distinguish transient infra issues (3, 5) from configuration mistakes (1, 2) from partial successes (4).
npm strategy
| Package | Purpose | Status |
|---|---|---|
packdog | CLI | Published — npm install -g packdog |
@packdog/* | Future packages (vite-plugin, create, etc.) | Planned |
Install globally: npm install -g packdog
CI usage
PACKDOG_TOKEN in the runner's secrets is all you need. The CLI does not require any "login" step on a fresh machine when the env var is set:
- run: npx packdog deploy --channel=test --dir=dist
env:
PACKDOG_TOKEN: ${{ secrets.PACKDOG_TOKEN }}
PACKDOG_PACKAGE_ID: ${{ secrets.PACKDOG_PACKAGE_ID }}Use a ci_* CI token created by an admin — packdog ci-tokens add <name> (auto-grants the org's sole project for single-project orgs), or the panel's CI tokens tab for multi-project scoping and Protected publishing. The token is scoped to the packages in the projects it is granted on; whether it may publish to protected channels is set per-project grant in the panel. A ci_* token is the only credential for CI: human sign-in (packdog login) needs a browser and is not for unattended runners.