CLI Reference
The portbay CLI shares the same portbay_lib core as the Tauri GUI. It acts as a client: lifecycle commands (start, stop, restart) require the PortBay daemon (the GUI app, or a future portbay daemon subcommand) to be running and exposing Process Compose on a discoverable port.
Typed without a management subcommand, portbay starts the local PortBay agent engine, similar to codex or claude:
portbay
portbay "inspect this repo and summarize the next fix"
portbay --json -P ollama -m qwen2.5-coder:latest "run the task"The command is lowercase on disk and case-insensitive wherever PortBay parses it as an agent id.
Install
The app ships the CLI inside its bundle. To put it on $PATH, open Settings → Advanced and enable the Command-line tool row — this symlinks the bundled binary to /usr/local/bin/portbay (one OS authorization prompt at most, VS Code's "install code command" model). The agent engine ships in the same bundle and is resolved through that symlink automatically; no second install step is needed. Homebrew installs (brew install portbay-app/portbay/portbay) land on $PATH directly.
Global options:
| Option | Meaning |
|---|---|
--json | Emit machine-readable JSON. |
--registry <PATH> | Override the registry file location. |
--pc-port <PORT> | Override the Process Compose daemon port. |
Commands
| Command | Purpose |
|---|---|
portbay list | List registered projects with live status when the daemon is reachable. |
portbay status [id] | Show one project's status, or all projects when no id is provided. |
portbay add <PATH> | Register a project from a folder path. Auto-detects .portbay.json if present. |
portbay remove <id> | Unregister a project and remove generated artifacts by default. |
portbay update <id> | Change a project's start command, environment or kind. |
portbay start <id> | Start one project. |
portbay stop <id> | Stop one project. |
portbay stop --all | Stop every running process. |
portbay restart <id> | Restart one project. |
portbay logs <id> | Print static log output for a project. |
portbay runtime pin <id> <lang@version> | Pin one project's runtime version, or clear it with --clear. |
portbay open <id> | Open the project URL in the default browser. |
portbay doctor | Grouped, flutter doctor-style health check across registry, daemon, routing/TLS, PHP, services, and account. |
portbay hosts <subcommand> | Manage PortBay's /etc/hosts block. |
portbay export <id> | Write <project_path>/.portbay.json. |
portbay completions <shell> | Generate shell completion scripts. |
portbay login [--email <addr> | --device] | Sign in to PortBay Cloud (GitHub OAuth by default; --email for a magic link; --device for a short code typed on another device). |
portbay license | Show the current account, tier, and entitlement limits. |
portbay logout | Sign out and clear the saved session. |
portbay token create|list|revoke | Manage account tokens for CI (PORTBAY_TOKEN). |
Agent mode — retired
portbay used to double as an agent CLI: anything that wasn't a management subcommand was handed to a bundled portbay-agent engine, so portbay "fix the failing test", portbay -p "plan the refactor" and portbay --json "run the task" all started an agent run.
That engine no longer ships. PortBay runs agent work on its own native in-process runtime instead of bundling a second engine. -p is PortBay's own one-shot now (see below); a bare prompt or an engine flag prints what happened and exits 2 rather than doing something else quietly:
$ portbay "plan the refactor"
portbay: `plan the refactor` is not a PortBay command.
The bundled `portbay-agent` engine that used to run free-text prompts and
agent flags typed at `portbay` has been retired. PortBay runs agent work on
its own native runtime now instead of shipping a second engine, and there is
no CLI entry point that takes a bare prompt.
portbay -p "<prompt>" run one prompt in this project and exit
portbay open the PortBay terminal
portbay tui --project <id> --card <id>
run a board card on the native runtime
portbay tasks list <project> read the board from the shell
portbay --help every command this build hasWhat replaces each old use:
| Was | Now |
|---|---|
portbay (interactive session) | portbay still opens PortBay's own terminal. |
portbay "<prompt>" / -i | portbay -p "<prompt>" for one answer, or put the work on the board and dispatch the card — portbay tui --project <id> --card <id> runs it on the native runtime. |
portbay --json "<prompt>" (headless NDJSON) | portbay -p "<prompt>" --output-format stream-json. The old agent_event / run_result NDJSON contract belonged to the retired engine; the stream is tasks run --json's. |
PORTBAY_AGENT_BIN | Nothing. No code path resolves an agent sidecar any more. |
Local-model board dispatches are unaffected in the place that matters: an Ollama or Cloud (BYOK) card still runs a full agentic read/edit/run loop, on the native runtime rather than through the bundled engine.
-p — one prompt, no terminal
portbay -p "<prompt>" runs one turn of a terminal session in the project the current directory belongs to, prints the answer and exits. With no value the prompt is read from stdin (git diff | portbay -p). Nothing is written to the board.
| Flag | |
|---|---|
--output-format text|json|stream-json | text (default) prints the answer alone; json one result object; stream-json every event as NDJSON with "v":1, ending in the result — the tasks run --json schema, with cardId: null and sessionId and result added. |
--mode auto|plan|manual | auto (default) applies edits; plan makes none; manual stops at the first edit and exits 4, since nobody is there to approve it. |
--agent <name>, --model <model> | The backend, chosen as the terminal chooses it when omitted. |
--timeout <secs> | Wall-clock bound; defaults to the project's automation.timeoutSec, 0 for none. |
Exit codes follow tasks run: 0 answered, 4 the run stopped for a human (a question, or a manual-mode edit), 5 it ended without answering, 6 it could not start (for example a project whose agent is an external CLI), 7 it passed --timeout, 130 Ctrl-C. Output never carries terminal escapes when stdout is not a terminal.
add
If the target folder contains a .portbay.json, add reads it and imports the project from that file. Otherwise it registers the project from the supplied flags.
A portbay.yml or .portbay.json arrives with the repo, so the commands it names (startCommand/start_command, pre- and post-start hooks, auto-start, and env such as NODE_OPTIONS or DYLD_* that changes what those run) are printed rather than applied. Read them, then re-run with --trust-commands to apply them. A hostname outside your PortBay domain suffix, a document root outside the project, or a hostname or port another project already uses is refused either way.
portbay add <PATH> \
--id <id> \
--name <name> \
--hostname <hostname> \
--kind next|vite|php|static|node|flutter|xcode|android|custom \
--port <port> \
--proxy <url> [--preserve-host] \
--start-command <command> \
--document-root <relative-path> \
--php-version <version> \
--web-server caddy|nginx|apache \
--https true|false \
--auto-start--kind defaults to custom. --https defaults to true.
--web-server picks the server behind the Caddy edge and defaults to caddy. A PHP site with no --start-command, or a static site, is served from its document root by the server you pick; any other project gets Nginx or Apache in front of its dev server or --proxy upstream. It is ignored for native app kinds (xcode, android, flutter, expo). The choice is checked with the server itself before anything is saved; a server that isn't installed, or a config it rejects, refuses the add. See Choosing a web server.
--proxy sends the hostname to an upstream instead of localhost:<port>: an IP on a VM or bridge network, an internal name, or a remote URL, such as http://192.168.64.2:8080, http://api.internal:3000 or https://example.com. The upstream receives its own hostname as Host unless you pass --preserve-host, which sends the project's hostname instead. An https:// upstream can't be combined with --web-server nginx|apache: Caddy reaches it directly and verifies its certificate.
runtime pin
portbay runtime pin <id> node@22
portbay runtime pin <id> --clearPins one project's runtime version. The language must be the one the project's kind runs on, and that version must be installed. The pin rewrites the project's .nvmrc, .node-version or .tool-versions so your terminal agrees, and a running project restarts on the new version at the app's next reconcile. --clear returns the project to its kind's default runtime.
update
portbay update <id> --start-command "pnpm dev --turbo"
portbay update <id> --env API_URL=http://localhost:4000 --env DEBUG=1
portbay update <id> --kind next--env sets the keys it names and leaves the project's other variables alone. --start-command "" clears the command. A kind not in portbay add --help's list is refused before anything is written. With the app running, start and restart wait for it to load the edit into Process Compose, so a restart straight after an update runs the new command.
remove
portbay remove <id>
portbay remove <id> --keep-artifacts--keep-artifacts leaves cert files and live Caddy route artifacts in place when reachable.
logs
portbay logs <id> --limit 200 --offset 0--limit defaults to 200. --offset defaults to 0 (newest).
doctor
portbay doctor
portbay --json doctorA flutter doctor-style environment report. Checks are grouped into categories; each category header shows the worst verdict among its rows ([✓] ok · [!] warning · [✗] fatal), and every row carries an inline fix hint.
| Category | Checks |
|---|---|
| Core | Registry loads (project count, schema version, domain suffix); Process Compose daemon reachability; /etc/hosts managed entries reconciled against the registry. |
| Web routing & TLS | Caddy and mkcert (bundled sidecars — see note); local certificate count under the certs directory. |
| PHP runtimes | Every detected PHP install (version, path, source), flagged when php-fpm is missing so it can't serve sites. |
| Services | dnsmasq resolver routing for the wildcard suffix; Mailpit (bundled sidecar); available database engines (MySQL, MariaDB, Postgres, Redis, Mongo, Memcached). |
| Account & sharing | Signed-in account, tier, and project cap; active tunnel count. |
PortBay-bundled sidecars (Caddy, mkcert, Mailpit, cloudflared) are never resolved from $PATH on macOS — they ship with the app, so a foreign install is never mistaken for PortBay's own. On Linux, dnsmasq is intentionally distro-managed: PortBay's sidecar wrapper invokes the system package. Live sidecar state isn't observable from outside the daemon, so these checks report as informational rather than a guessed process state, mirroring portbay sidecar status.
doctor exits 0 even when warnings are present; it returns a non-zero code only when a check is fatal (e.g. the registry fails to load). With --json it prints an array of categories, each with a verdict and a checks array of { check, verdict, detail } objects.
hosts
portbay hosts list
portbay hosts add <hostname> --ip 127.0.0.1
portbay hosts remove <hostname>
portbay hosts clear
portbay hosts reconcilehosts add defaults --ip to 127.0.0.1. All write operations (add, remove, clear, reconcile) try the bundled portbay-hosts-helper sidecar first; they fall back to direct /etc/hosts writes, which require elevated privileges.
completions
portbay completions bash
portbay completions zsh
portbay completions fish
portbay completions powershelllogin
portbay login # GitHub OAuth (opens browser)
portbay login --email <addr> # email magic link
portbay login --device # short code, approved from any other deviceDrives the GitHub OAuth or email magic-link flow from the terminal, then stores the session in the OS keychain shared with the GUI. Polls for up to 5 minutes. Prints the signed-in username and tier on success. If the browser asks you to confirm a code, check it matches the one the terminal printed.
--device is for a machine with no browser (SSH, a container): it prints a code such as ABCD-EFGH and a URL. Open the URL on any device, enter the code, and the terminal finishes signing in. The code expires after 10 minutes. Under --json the code is written to stderr as one JSON line (userCode, verificationUri) and stdout carries only the final account document.
Non-interactive auth (CI)
export PORTBAY_TOKEN=pbt_…
portbay license --jsonPORTBAY_TOKEN authenticates every PortBay Cloud call without a stored session and takes precedence over one. It is never written to disk, never refreshed, and is removed from the environment before PortBay starts any child process. A value that does not start with pbt_ is refused (exit 2). Under the token, license and resync fetch the entitlement live rather than reading the cache.
token
portbay token create ci-deploy --expires-in-days 90
portbay token list
portbay token revoke <id>Creates, lists and revokes account tokens. create prints the token once; store it as the PORTBAY_TOKEN secret in your CI. --expires-in-days takes 1-365. These commands need your own sign-in (portbay login): run under PORTBAY_TOKEN, they exit 2 with a message saying so.
license
portbay licensePrints the cached effective entitlement: account login, tier (anonymous / free / pro), project cap, sync, and mail entitlements.
logout
portbay logoutClears the saved session and cached entitlement from the OS keychain.
Exit Codes
| Code | Meaning |
|---|---|
0 | Success |
1 | Generic failure |
2 | User input error (bad project id, missing argument) |
3 | Daemon unreachable |
4 | Port conflict |
5 | Readiness timeout (reserved) |
6 | Permission denied (hosts write requires elevated privileges) |
