Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

piquelctl

piquelctl is the small operator client for piqueld. It uses the public piqueld-client contracts and talks to the daemon over a Unix socket by default.

piquelctl commands lists every command with its arguments and options.

Installing

On the daemon’s host, the NixOS module installs piquelctl, and the other installations install it beside the daemon. On any other Linux or macOS machine, install Rust with rustup, then install piquelctl from crates.io:

cargo install --locked piquelctl

macOS support

On Apple Silicon, Nix users can run nix build .#cli or nix run .#cli -- --help. On macOS the default flake package is also the CLI; daemon and dashboard packages are Linux only. nix develop provides the CLI development tools, and just validate-cli lints and tests the CLI and its shared client/core crates. just build-cli builds the release binary. CI runs just validate-cli on macos-latest when the CLI or its shared crates change. nix-darwin users can import darwinModules.piquelctl to install it with profiles; see the NixOS module.

The daemon runs on Linux. With its listen mode set to tailscale or both, connect from a tailnet peer using its IP address or MagicDNS name:

piquelctl --allow-insecure-http --url http://linux-host:7845 login
piquelctl --allow-insecure-http --url http://linux-host:7845 status

For persistent configuration, set a named profile’s url to http://linux-host:7845 and select it with --profile and --allow-insecure-http. See daemon configuration.

Both platforms discover system and user profiles at runtime, including binaries built with Cargo. See connection profiles.

Commands

An application owns the saved manifest; its environments deploy it, each with its own deployments, status, volumes, generated secrets, routes, and network. A manifest that declares environments gets exactly those when it is saved or fetched from the branch spec.manifest names; otherwise applications have an environment named production from creation. env commands select an environment by name or stable ID; ENV may be omitted only when the application has exactly one. app deploy, app reconcile, app stop, app start, and app logs act on the application’s only environment and fail with an “environment required” input error naming the environments when it has several; they never pick one. With several environments, app delete requires --environments a,b naming every one. env delete keeps the application and retains the environment’s named volumes. Environment creation, renames and deletions are conditioned on the inspected application revision like other mutations (--expected-generation, --force), and advance it, so a command based on an earlier inspection fails instead of acting on a renamed or deleted environment.

Declared environments exist as long as their [spec.environments.<name>] block does: env create, env rename (to or from a declared name) and env delete refuse them with environment_declared, naming the environment (remove the block before deleting one, rename the block to rename one). When the block sets branch, commit, promote_from or sync, env source, env branch and env sync refuse it too; change the manifest instead. app plan lists, under Environments:, what saving a manifest would create (+), change (~), or no longer declare (!); app show lists how the environments differ from the saved manifest, including environments it no longer declares, which keep running until env delete. app apply --deploy deploys every declared environment that builds and waits for each; promoted ones wait for env promote.

Each environment of a repository-backed application follows its own branch of the application’s repository; the repository URL and manifest path stay on the application (app repository url|path). env create --branch chooses the branch, by default the one spec.manifest names, and --commit pins a commit. env branch points an existing environment at another branch, or pins or unpins (without --commit) a commit; nothing is redeployed until its next deployment. Both fail with manifest_repository_required for applications without a repository. env list and env show report the source as saved, as the branch, e.g. main or main@<commit>, or as promoted from <name>.

Deploying on push is a repository setting: app repository sync APP poll [--interval SECONDS] lists the branches every interval (300 seconds by default), webhook when GitHub reports a push, and off (the default) deploys only when asked; app repository connect takes the same as --sync and --interval. Every preview, and every environment that opted in with env sync APP ENV on and follows an unpinned branch, then redeploys when its branch moves past the head of its last deployment; nothing follows before its first deployment, and a one-off --branch/--commit deployment stays until the branch moves. env sync … off opts it out again; env sync needs no revision and asks no confirmation. Turning sync on, or opting in while it is on, needs apps:deploy too.

notification manages notification destinations. notification set NAME creates a destination, or changes one; options it omits keep the current settings, and a new destination receives every category but deployment_succeeded about every application, without previews. The URL is read from --url-file or --url-stdin, never an argument, and is required to create one. --kind json|discord picks the payload, --enabled true|false pauses it, --categories a,b replaces the categories it receives, and --message CATEGORY=TEXT (repeatable) sends text with a category’s notifications, such as a Discord role mention <@&ROLE_ID>, which also adds the category; --no-message CATEGORY removes it. --apps a,b limits it to those applications, by name or ID, --all-apps selects every one, and --previews true|false chooses previews:

piquelctl notification set ops --kind discord --url-file discord-url
piquelctl notification set ops --message 'deployment_failures=<@&123456789012345678>'
piquelctl notification set ops --apps shop,notes --previews true
piquelctl notification test ops

notification test sends a test notification at once and fails unless the receiver acknowledges it; list shows destinations without URLs, and delete removes one (--yes skips confirmation). Changing destinations needs notifications:manage, and listing them system:read or notifications:manage.

List the latest deliveries, newest first:

piquelctl notification deliveries --limit 20

Each line shows the delivery’s ID, destination, category, subject, state and attempts, when it last changed, and its last error. A pending delivery shows when it is next attempted instead. The page holds 50 deliveries unless you pass --limit, up to 100. Pass the printed --cursor to read the next page.

Try a failed delivery again, with its destination’s current settings:

piquelctl notification retry <delivery-id>

Listing deliveries needs system:read, and retrying them system:operate. The daemon refuses to retry a delivery that hasn’t failed, that its destination no longer receives, or whose incident has since closed or opened again. It also refuses a failure after its destination received the recovery that followed it. The command then fails with delivery_not_retryable and exit code 3, and its message says why. See notifications.

env stop APP [ENV] confirms, then stops an environment: the gateway withdraws its routes at once and its services scale to zero replicas, keeping their configuration, volumes and secrets. It waits until its runtime reports stopped, unless --no-wait, fails if the deployment the stop runs again fails, and ends early if the environment is started or deployed meanwhile. A stopped environment stays stopped until env start, env reconcile, a deployment, a restore, or a promotion into it; pushes don’t deploy it meanwhile, and sync deploys the head it missed once it starts. env start APP [ENV] confirms, then starts it again, retrying its latest operation like env reconcile and waiting for it. Stopping a deployment in progress lets it finish with no replicas. Both need apps:deploy; --expected-generation is optional, as for reconcile. env list shows a stopped environment’s state as stopped, and env show says so. Previews are deleted instead.

env telemetry APP ENV on grants an environment the daemon’s telemetry: its receiving service, marked with app service telemetry APP SERVICE on, gets the traces, metrics and logs of every application. It needs telemetry:grant, which no preset includes. off revokes it, which stops export at once. Either way the controller mounts or unmounts the socket within seconds, which restarts the service; nothing is deployed, and env telemetry needs no revision and asks no confirmation. Previews are never granted. env show reports whether the environment is granted, which service receives it, and whether the daemon is connected; status lists the receivers of the environments you can read next to the [telemetry] endpoint. app show reports the setting and the last check of the repository, with its error; env show, preview show, and preview list report whether each one follows pushes (following, off, off (pinned), off (not opted in), off (stopped), off (waiting for a deployment)) and the branch head of its last deployment. app repository webhook show APP prints the payload URL to configure in GitHub, once the daemon sets ingress.webhook_hostname, and whether a secret exists; app repository webhook rotate APP confirms, then generates a new secret, replacing the previous one, and prints it: it is never shown again. events names sync deployments’ actor, sync:poll or sync:webhook.

The manifest is shared; values that differ between environments come from manifest variables. Environments that render the same route hostname conflict with hostname_conflict, naming the sibling environment that reserves it. env rename fails with environment_configured while the manifest the environment deploys (for a repository-backed one, the one last fetched from its branch) has a [spec.environments.<name>] block for the old or the new name, since renaming would change which block applies: remove the block, rename, then add it back under the new name. env show lists each variable’s value in that manifest, or that it has none, and says when a repository-backed environment has fetched nothing yet.

Each successful preparation of an environment’s deployment records an immutable release (previews never record one): the manifest it deployed (and its commit when repository-backed), each service’s image (a registry digest, or the local image ID and commit of a Git build), and the build inputs those images came from. Releases belong to the application, so deleting an environment keeps them, and environments that prepared the same content share one release. app releases lists them newest first, twenty per page, with each service’s image and provenance, and whether its images are still present: all present, missing registry images that deploying it pulls again by digest, or missing builds, which mean it can’t be deployed again (image cleanup keeps each environment’s last images.keep_deployments successful deployments). --json adds the manifest and build inputs. env show reports the release the environment’s current target runs (none before its first prepared deployment). Each release also lists the deployments that received it by promotion or by release ID.

A promoted environment never builds: it only receives releases from another environment of the application. env create --promote-from ENV creates one, and env source APP ENV --promote-from ENV converts an existing one (name or ID); env source --tracking returns it to the saved manifest or the branch spec.manifest names, and env branch to another branch. These deploy nothing, take --yes, --force and --expected-generation like env branch, and fail with promotion_cycle or promotion_source_invalid for an impossible source. env delete of an environment others promote from fails with promotion_source_in_use, naming them. env deploy and app deploy of a promoted environment fail with environment_promoted.

env promote APP ENV deploys the release of the source environment’s current deployment, after confirmation (--yes), and waits for it unless --no-wait. --deployment ID refuses to promote anything but that deployment (promotion_source_changed once the source moved on); --release ID deploys an earlier release of the application with the environment’s current secrets. --expected-generation N pins the application revision; by default the inspected one is sent. It fails, before deploying anything, with promotion_source_not_ready unless the source’s current deployment succeeded and runs healthy now, release_incompatible, image_unavailable, or secrets_unavailable listing every unusable secret. env source --promote-from fails with application_busy while a deployment of the environment is in progress. environment_not_promoted means the environment builds its own source. env promote --plan prints the release and its provenance and images, its origin, the rendered changes, the runtime plan, new (empty) volumes, and each unusable secret, without changing anything. It exits with the conflict code 3 when the plan is blocked, as it is by each missing image that can’t be pulled again (image_unavailable), or lists secrets. --release ID --plan plans an earlier release for any environment.

env history APP [ENV] lists an environment’s deployments, newest first, three per page (--cursor for older ones): each one’s ID, outcome, where it came from, its release, and which is the current target and the last successful one. An automatic rollback shows as “rolled back from deployment <failed> to deployment <restored>”.

env restore APP ENV DEPLOYMENT deploys an earlier deployment again exactly as it ran: its rendered manifest, its images and the secret versions it pinned, never newer ones. It is recorded as a new deployment restored from that one, which becomes the maintained target, so drift repair keeps it. It works for any environment, promoted ones included, and needs no application revision. It asks for confirmation (--yes), then waits unless --no-wait, reporting like env deploy. preview history and preview restore do the same for a preview.

A restore fetches and builds nothing, and never runs jobs: re-running an old deployment’s migrations against newer data is exactly what it must avoid. It does not revert data either. Volume contents and migrations that ran since stay as they are, so the restored code must cope with them; expand/contract migrations keep old code compatible.

env rollback APP ENV automatic|manual|default chooses whether a failed rollout restores the environment’s last successful deployment on its own (see rollback). default removes the setting: promoted environments then roll back automatically, the others manually.

Before deploying anything, a restore fails with:

  • deployment_not_restorable for a deployment that never succeeded.
  • image_unavailable, naming the service and image, when an image is gone and can’t be pulled again by digest. A build is never rebuilt. Image cleanup keeps each environment’s last images.keep_deployments successful deployments, and only a preview’s current one.
  • restore_secrets_unavailable, naming the secrets whose pinned versions were deleted or discarded by key recovery since.
  • secret_access_denied for a stored secret whose access no longer allows the environment.

A preview is a disposable deployment of one branch of a repository-backed application. preview commands address a preview by stable ID, then slug, then branch with --slot for a slotted one; env commands and env list never show or act on previews. preview create creates and deploys the preview of --branch and --slot, or returns the existing one and its latest deployment without redeploying it, and fails with preview_requires_repository for other applications. preview deploy redeploys the head of its branch, replacing a local snapshot the preview runs. None of them needs the application revision. preview create fails with preview_limit_reached when a new preview would exceed the daemon’s [previews] limits, listing the previews it counts with their branch, slot, slug and last deployment so you can delete one; repeating an existing creation always succeeds. preview list and preview show report each branch as exists, moved, gone, local or unknown, read with git ls-remote; show also lists the preview’s URLs, and each service its deployment runs with default limits or fewer replicas than its manifest asks for. preview delete removes the preview and every volume it created, after confirmation. preview prune --branch-gone lists the previews whose branch is gone, confirms, and deletes them; the daemon checks each branch again, keeps any it can no longer confirm gone, and deletes nothing while the repository cannot be read. Previews whose branch state is unknown are kept, with a warning. preview create, deploy, delete and prune wait for their operations unless --no-wait; delete and prune confirm unless --yes. Creating, deploying and logs need the apps:deploy and logs:read permissions as for environments; deleting and pruning need apps:delete.

preview deploy <app> --local deploys the Git worktree of the current directory without pushing it: its committed, uncommitted and untracked files, except ignored ones. A tracked file that is ignored, such as a committed .env template, is sent as committed, never with its local edits, even when deleted. Before uploading, it lists the uncommitted files it sends (Info, hidden with --quiet), and the snapshot’s size and the commits it carries. It deploys to the preview of the current branch and --slot, creating it first if needed exactly as preview create does, within the same limits; on a detached HEAD, name the branch with --branch. The snapshot is a commit on top of HEAD made with a temporary index and object directory, so the branch, index, working tree, stash, refs and objects of the repository never change. It is uploaded as a Git bundle, thin against the nearest ancestor of HEAD that a remote-tracking branch has, which the daemon fetches from the application’s repository: only unpushed commits are sent. When there is no such ancestor, or the daemon cannot fetch it (snapshot_base_unavailable), the command fails rather than send the whole history; push the base, or pass --full-history to upload every commit of HEAD. Snapshots whose files take more than the daemon’s checkouts.max_bytes, or number more than it accepts, are refused before anything is staged, naming the largest files, and bundles over its snapshots.max_upload_bytes before they are uploaded; the daemon refuses both with snapshot_too_large too. Bundles Git cannot verify fail with snapshot_invalid. The daemon reads the manifest from the snapshot at the application’s manifest path and builds repository = "self" services from it, like any preview deployment, with the same build priority and limits. Submodules, nested repositories, SHA-256 repositories, partial clones, and files staging would pass through a configured Git filter driver, such as LFS’s, are refused, since those filters write into the repository. A large first upload may need a longer --timeout; the daemon gives up on a body that stalls for 30 seconds, or after 10 minutes (snapshot_timed_out). Uploading needs previews:deploy, which apps:deploy includes.

A preview that runs or is deploying a local snapshot reports its branch as local, with the branch’s head if it was pushed: preview prune never deletes it, so delete it with preview delete. Deploy on push leaves the snapshot running until the branch’s head moves, or, for a branch never pushed, until it is pushed; then it deploys that head. preview show and the deployment history show the snapshot commit and its base.

env url and preview url print the URL of every route the environment’s or preview’s current deployment renders, with its route name and state: ready, or pending with what it is waiting for, one line each, as env show, deployments and wait list them:

URL: https://notes.example.com (web) ready
URL: https://admin.notes.example.com (admin) pending: HTTPS (No DNS-01 certificate yet)

--route NAME prints only the URL of the named route, and warns on stderr while it is pending. The daemon derives the state from what it already observes (see URL readiness); the CLI never probes a URL itself, so a URL that is known is not necessarily ready.

env wait and preview wait wait for one deployment: --deployment ID, or the latest one when wait starts. They never follow a newer deployment. With --ready runtime (the default), the deployment must succeed and its runtime must be observed, with every service healthy (converged, with every desired replica healthy); --ready routes also waits until every URL is ready. If a newer deployment, or the environment’s deletion, supersedes it, wait exits 3 with deployment_superseded; if it fails, 5; at the command --timeout, 4, which ends only the local wait. Use a longer --timeout than the default 30 seconds for builds. On success, wait prints the deployment like env deploy does.

--socket PATH selects a Unix socket. --url URL selects an explicit HTTP or HTTPS origin such as http://127.0.0.1:7845/; the two transport options are mutually exclusive. The default socket is /run/piqueld/piqueld.sock.

Remote IP addresses and DNS names are accepted. HTTPS is supported; embedded URL credentials, non-root paths, queries, and fragments are rejected. DNS resolution and connection setup share the request timeout; redirects are not followed. Remote HTTP authentication, including login, requires --allow-insecure-http when using separate transport encryption such as Tailscale. The client does not verify tailnet membership or add encryption with this flag. Prefer HTTPS, such as a daemon’s tailnet node at https://piqueld.<tailnet>.ts.net. Loopback HTTP, including localhost and its subdomains, and Unix sockets need no opt-in. When the daemon runs a tailnet node, status also reports its login state, certificate expiry, and whether auth.public_url matches the node. It also lists each DNS provider with its zones, health and whether it manages route records, and each DNS-01 certificate with its hostnames, expiry and last error. piquelctl dns refresh checks the providers’ credentials and zones now instead of at the next hourly discovery, then prints the same DNS lines. status also reports how many images this installation built that image cleanup kept, and the total size of those it removed since the daemon started.

status answers whether the daemon and its subsystems are up. Two more read-only commands, both requiring system:read, show what the dashboard’s Daemon status and Host settings pages do:

  • piquelctl resources shows the daemon’s resource measurements: uptime, resident memory, CPU usage, the database and its write-ahead log, free disk on the data directory’s filesystem, retained events, diagnostics and build output, running and queued operations, and notification deliveries awaiting a retry or failed. The daemon measures at most every 5 seconds and averages CPU usage since its previous measurement, so its first measurement has none. What the host cannot measure is unavailable, never zero.
  • piquelctl settings shows the daemon’s effective configuration, grouped as on the Host settings page. Credentials appear only as the file they are read from. Change settings in the daemon’s TOML and restart it. Each read is recorded in the audit trail.

Global --timeout DURATION defaults to 30s. Durations are positive integer milliseconds (ms), seconds (s), minutes (m), hours (h), or days (d); a bare integer is interpreted as seconds. The timeout bounds the complete command, except login, which waits for browser approval, and an app exec or preview exec session; interactive confirmation prompts do not consume it. Read requests can use the full remaining command budget. Idempotent mutations retry one transport failure if time remains; a request that exhausts the command budget cannot be retried. Manifest files must be regular UTF-8 files no larger than 2 MiB, matching the daemon’s API request body limit.

Use --json for machine-readable output. JSON is made only from public API DTOs and the small CLI composition objects below; diagnostics and progress are written to stderr, so stdout remains valid JSON.

CommandJSON output
profiles{ "profiles": [{ "name": string, "endpoint": string, "default": bool }] }
statusSystemStatus
resourcesDaemonStats
settingsHostConfiguration
doctor{ "connection": { "endpoint": string, "endpoint_source": string, "credential": string }, "checks": [SystemCheck] or null }; checks is null when the daemon could not be asked
dns refreshDnsStatus
loginUser
whoamiSession
account list / account access / account update{ "accounts": [Account], "operator_sessions": [OperatorSessionView] }
account invite / account enroll{ "url": string }
account passkey list[PasskeyView]
account invitation list[InvitationView]
token create{ "token": string }
token list[CredentialView]
logout / token revoke / account delete / account passkey rename|remove / account invitation revokeNone
audit{ "items": [AuditEvent], "next_cursor": string or null }
audit verifyAuditVerification
setup-linkSetupLink
sign-in-link / recover-adminOperatorLink
app list{ "items": [{ "application": ApplicationSummary, "environments": [EnvironmentRow] }], "next_cursor": null }
app show{ "application": ApplicationView, "environments": [EnvironmentRow] }
env list[EnvironmentRow], where EnvironmentRow is { "environment": EnvironmentView, "status": EnvironmentStatusView or null }
env showEnvironmentDetailView (including its urls, links and telemetry), with "stored": [StoredSecret]
env url / preview url[RouteUrl]
env url --route / preview url --routeRouteUrl
env wait / preview waitDeployment
env create / env rename / env branch / env sync / env telemetry / env source / env stopEnvironmentView
app repository webhook showWebhookView
app repository webhook rotateWebhookSecret
env promote --no-wait{ "application_id": string, "environment_id": string, "deployment_id": string, "operation_id": string, "release_id": string, "origin": DeploymentOrigin }; the deployment and operation IDs are equal
env promoteSame, with "outcome": OperationState and "operation": Operation
env promote --planPlanView, with release
app logs / env logsApplicationLogs
app validate{ "application": string }
app exec / preview execNone; the command’s raw output
app planPlanView
app route list[RouteStatus], each with "environment": string (its name)
app file list[ServiceFile], each with "service": string
app service secret list[SecretMount], each with "service": string
app export list / app import list[Export] / [Import], as saved
app generated-secret list[SecretDeclaration], as saved
app secret list[StoredSecret]
app secret set / accessStoredSecret
env secret list[SecretMetadata]
env secret regenerateSecretMetadata
app secret delete / env secret delete{ "deleted": string }
secrets recover-keySecretKeyRecovery
notification list / set[NotificationDestination]
notification testDestinationTest
notification delete{ "deleted": string }
notification deliveries{ "items": [NotificationDelivery], "next_cursor": string or null }
notification retryNone
builds list{ "items": [BuildRecord], "next_cursor": string or null }
builds logsBuildLogPage
app create / app rename / field editsSavedApplication; --deploy uses the same output as app apply --deploy
app manifestSaved TOML as a JSON string
app releases{ "items": [ReleaseView], "next_cursor": string or null }
app applySavedApplication with null operation_id and empty operation_ids
app apply --deploy --no-waitSavedApplication with the deployment operation IDs in operation_ids, the first also in operation_id
app apply --deploy{ "saved": SavedApplication, "outcome": OperationState, "operation": Operation, "operations": [Operation] }; outcome and operation are the first deployment’s, operations every one
app delete --no-wait{ "deleted": DeletedApplication, "volumes_retained": true }
app delete{ "deleted": DeletedApplication, "outcome": "deleted", "volumes_retained": true }
env delete --no-wait{ "accepted": AcceptedOperation, "volumes_retained": true }
env delete{ "accepted": AcceptedOperation, "outcome": "deleted", "volumes_retained": true }
preview create --no-waitCreatedPreview and the fields of Deployment
preview createCreatedPreview and the fields of Deployment, with outcome and urls
preview list[PreviewView]
preview showPreviewView
preview deploySame as env deploy
preview deploy --local --no-waitCreatedPreview and the fields of Deployment, with local true
preview deploy --localCreatedPreview and the fields of Deployment, with local true, outcome and urls
preview logsApplicationLogs
preview delete --no-wait{ "accepted": AcceptedOperation, "volumes_retained": false }
preview delete{ "accepted": AcceptedOperation, "outcome": "deleted", "volumes_retained": false }
preview prune[DeletedPreview]
operation --no-waitOperation
operationOperation
app/env reconcile / start / deploy{ "accepted": AcceptedOperation, "operation": Operation } and the fields of Deployment, with outcome and urls
app/env reconcile --no-wait / stop --no-wait / start --no-wait / deploy --no-waitAcceptedOperation (operation_id, environment_id, generation) and the fields of Deployment
events{ "items": [Event], "next_cursor": string or null }
analyticsDeploymentAnalytics

Deployment is the CLI’s summary of one deployment of an environment or preview, flattened into the results above:

FieldValue
application_id, environment_idStable IDs
slugThe environment’s name, or the preview’s slug (env.slug)
deployment_id, operation_idThe deployment, and the operation that runs it (the same ID)
release_idThe release it runs; null until prepared, and for previews
branch, commitWhere its manifest was read from; null until fetched, and for saved manifests. For a local snapshot, the snapshot commit
localWhether it deploys a snapshot of a local worktree (preview deploy --local)
baseThe commit of the application’s repository a local snapshot was uploaded against; null when it was uploaded whole, and for other deployments
outcomeIts OperationState; omitted with --no-wait
urls[RouteUrl] of its routes once it runs; omitted with --no-wait and when it does not run (superseded)

The DTO fields are defined by the versioned API and the piqueld-client crate.

With --json, stderr is one JSON object per line too, keyed by its kind: {"info":{"message":…}}, {"warning":{"message":…}}, {"progress":{"task":…,"message":…,"outcome":…}} (outcome once the task finished), {"prompt":{"message":…}} for a confirmation asked on a terminal, and, when the command fails, exactly one {"error":{"code":…,"message":…,"details":…,"request_id":…}}. An API error keeps the daemon’s error envelope: its code, message, details and request_id. Failures the CLI detects itself use the same shape with a stable CLI code and no request_id:

CodeExitMeaning
usage2Invalid arguments
invalid_input2Invalid input, an unknown name, or an unconfirmed action
invalid_configuration2Invalid connection configuration; details.configuration_source
invalid_endpoint2A rejected endpoint; details.endpoint_source
connection_failed4The daemon could not be reached; details.endpoint and endpoint_source
request_timeout4A request exceeded --timeout; also details.timeout and timeout_source
timeout4The command exceeded --timeout; server-side work goes on
unavailable4The daemon stopped answering, e.g. an app exec stream ended before the command exited
invalid_response1The endpoint does not serve the piqueld API
conflict3An ambiguous name or a blocked plan
deployment_superseded3wait’s deployment was superseded; details.deployment_id and superseded_by
operation_failed5An awaited operation failed; details.operation
checks_failed6doctor found a failed installation check
interrupted130Ctrl-C
failed1Any other failure

Without --json, errors stay human-readable, as below.

Agents

Agents drive environments and previews with --json: stdout is one result document, stderr one JSON event per line, and the exit code says what happened. A typical run creates, waits, reads the URL and logs, then deletes.

For a preview of a branch:

created=$(piquelctl --json preview create notes --branch feat/login --slot agent-2 --no-wait)
slug=$(jq -r .slug <<<"$created")
deployment=$(jq -r .deployment_id <<<"$created")
piquelctl --json --timeout 15m preview wait notes "$slug" --deployment "$deployment" --ready routes
url=$(piquelctl --json preview url notes "$slug" --route web | jq -r .url)
piquelctl --json preview logs notes "$slug" --service web --tail 100
piquelctl --json preview delete notes "$slug" --yes

To test uncommitted work without pushing it, from the worktree:

deployed=$(piquelctl --json preview deploy notes --local --slot agent-2 --no-wait)
slug=$(jq -r .slug <<<"$deployed")
piquelctl --json --timeout 15m preview wait notes "$slug" --deployment "$(jq -r .deployment_id <<<"$deployed")" --ready routes

Its result has local: true, the snapshot commit and its base. Run it again after each change; each run deploys a new snapshot to the same preview.

For an environment, which env create adds without deploying:

piquelctl --json env create notes staging --branch main --yes
deployment=$(piquelctl --json env deploy notes staging --yes --no-wait | jq -r .deployment_id)
piquelctl --json --timeout 15m env wait notes staging --deployment "$deployment" --ready routes
url=$(piquelctl --json env url notes staging --route web | jq -r .url)
piquelctl --json env logs notes staging --service web --tail 100
piquelctl --json env delete notes staging --yes

Passing the deployment_id from the deploy or create result makes wait follow exactly that deployment, even if someone deploys again in between. Without --no-wait, deploy and create wait for the deployment themselves and return its outcome and urls, which can still be pending: wait --ready routes is what waits for them to be ready.

Branch on the exit code, then read the error event on stderr:

ExitMeaningTypical reaction
0Done; for wait, as ready as askedContinue
2Invalid command or input (usage, invalid_input, API validation codes, snapshot_too_large, snapshot_invalid)Fix the command; retrying will not help
3Conflict: deployment_superseded, preview_limit_reached (its details list the previews to delete), a revision preconditionRead the newer state; wait for details.superseded_by, or delete a preview
4Unavailable or timed out (connection_failed, timeout)Retry the read or wait; server-side work was not cancelled
5The deployment failed (operation_failed; details.operation names the phase and error)Read logs, fix, deploy again
6doctor found a failed check (checks_failed)Follow each failed check’s next_step

Installation checks

piquelctl doctor shows how this CLI connects (the endpoint, where it came from, and which credential it sends), then asks the daemon to check its installation (GET /api/v1/system/checks, which needs system:read):

CheckPasses when
Public URLauth.public_url is HTTPS and its /health answers piqueld’s liveness response from the daemon’s host with a trusted certificate
Tailnet nodeThe daemon’s node is logged in, holds a valid certificate, and serves auth.public_url
Docker EngineDocker answers; with ingress enabled, it is Engine 28+ with API 1.48+
Ports 80/443With direct ingress, they are held by the running managed Caddy gateway, or nothing on the host listens on them
Secret keysecrets.key is a private file matching the stored verifier, or not needed yet
API socketThe runtime directory passes startup’s checks, and the socket is 0660 with the daemon’s group
BackupsAlways skipped: piqueld does not take backups yet

Each check is pass, warn, fail, or skip (not applicable, such as the tailnet node while [tailscale] is disabled), with a next step when action is needed. doctor exits 6 when any check fails; warnings keep exit 0. Checks never change the installation. doctor makes a single request bounded by --timeout; when it fails, the connection is still reported, with checks null in JSON, before the error.

The checks run where the daemon runs. A reachable public URL means the daemon’s host reaches it; browsers elsewhere also need DNS and network access, and may reach it when the host cannot, such as without NAT hairpinning. Likewise, doctor reaching the daemon proves this CLI’s connection, not the browser’s.

Connection failures

Use piquelctl status to check whether the selected endpoint responds with the piqueld API, or piquelctl doctor for the resolved connection and installation checks. Connection diagnostics are available on failures from every command.

Failures identify the effective endpoint and its source (flag, environment, profile and file, or built-in default), preserve the observed cause, and suggest a check. Timeout failures also show the effective timeout and its independent source. For example:

Error: piqueld API request failed: opening connection: No such file or directory (os error 2)
  Endpoint: Unix socket /tmp/piqueld.sock
  Endpoint source: flag --socket
  Hint: Check the socket path and whether the daemon has created its socket.

Configuration failures identify the source that needs attention; they do not claim an effective endpoint before resolution succeeds. Rejected URLs and TOML source excerpts are omitted because they can contain credentials. Malformed profile files report a location and a safe error category instead.

These diagnostics use the original request and resolved configuration. They do not probe other targets, inspect permissions, or check database, Docker, or Swarm readiness; doctor checks the installation. Unexpected HTTP responses and invalid API data include connection context; ordinary application errors keep their existing reporting. With --json, the same facts are the error’s details (see the JSON errors above); with --quiet, diagnostics stay human-readable. Exit codes and successful output are unchanged.

Field editing

Application commands live under piquelctl app. Manifest import (app apply) and planning (app plan) remain available, but ordinary editing sends only the selected setting.

piquelctl app create notes --yes
piquelctl app service add notes web nginx:stable --yes
piquelctl app service replicas notes web 3 --yes
piquelctl app service env set notes web RUST_LOG debug --yes
piquelctl app service env remove notes web RUST_LOG --yes
piquelctl app service command notes web --yes -- /usr/bin/server
piquelctl app service arguments notes web --yes -- --listen "0.0.0.0:8080"
piquelctl app service depends-on notes web --yes -- postgres
piquelctl app service rollout notes worker --order stop-first --monitor-seconds 60 --yes
piquelctl app service telemetry observability collector on --yes
piquelctl app service add notes worker --git https://example.com/app.git --branch main --yes
piquelctl app service source git notes web https://example.com/app.git --target runtime --build-arg ORIGIN=https://notes.example.com --yes
piquelctl app service source git notes web https://example.com/app.git --branch main --yes
piquelctl app service source git notes web self --dockerfile build/Dockerfile --yes
piquelctl app service source branch notes web release --yes
piquelctl app service source commit notes web --clear --yes
piquelctl app service source dockerfile notes web build/Dockerfile --yes
piquelctl app service source context notes web build --yes
piquelctl app service source image notes web nginx:stable --yes
piquelctl app service cpu notes web 500 --yes
piquelctl app service memory notes web 268435456 --yes
piquelctl app service cpu notes web --clear --yes
piquelctl app volume add notes data --yes
piquelctl app service mount set notes web data /var/lib/data --yes
piquelctl app service mount remove notes web /var/lib/data --yes
piquelctl app service secret add notes web database-password /run/secrets/database-password --yes
piquelctl app service secret add notes web '${{ vars.stripe_key }}' /run/secrets/stripe --yes
piquelctl app service secret list notes web
piquelctl app service secret remove notes web /run/secrets/stripe --yes
piquelctl app file add notes web /etc/app/motd --content hello --yes
piquelctl app file add notes web /etc/prometheus/prometheus.yml --from ./prometheus.yml --yes
piquelctl app file add notes web /etc/otelcol/config.yaml --path otel/config.yaml --mode 0440 --yes
piquelctl app file list notes web
piquelctl app file remove notes web /etc/app/motd --yes
piquelctl app volume remove notes data --yes
piquelctl app route add notes notes.example.com web 3000 --visibility public --yes
piquelctl app route add notes admin.notes.example.com admin 8080 --yes
piquelctl app route add notes '*.tenants.notes.example.com' web 3000 --yes
piquelctl app route redirect notes www.notes.example.com https://notes.example.com --visibility public --yes
piquelctl app route visibility notes admin.notes.example.com private --yes
piquelctl app route name notes notes.example.com web --yes
piquelctl app route list notes
piquelctl app route remove notes notes.example.com --yes
piquelctl env visibility notes staging private --yes
piquelctl app job set notes migrate web --timeout-seconds 600 --yes -- notes migrate
piquelctl app job set notes migrate web --volume-access exclusive --yes -- notes migrate
piquelctl app job set notes migrate web --stop-service on --yes -- notes migrate
piquelctl app job set notes migrate web --env OWNER_PASSWORD_FILE=/run/secrets/owner --secret owner-password=/run/secrets/owner --yes -- notes migrate
piquelctl app job move notes migrate 1 --yes
piquelctl app job remove notes migrate --yes
piquelctl app export set auth api --applications notes,billing --yes
piquelctl app export set auth api --applications notes --previews --yes
piquelctl app export list auth
piquelctl app export remove auth api --yes
piquelctl app import add notes auth api --yes
piquelctl app import add notes auth api --environment staging --yes
piquelctl app import list notes
piquelctl app import remove notes auth api --yes
piquelctl app variable set notes domain piquel.fr --yes
piquelctl app variable set notes domain staging.piquel.fr --env staging --yes
piquelctl app variable set notes web_replicas 3 --env production --yes
piquelctl app variable unset notes web_replicas --env production --yes
piquelctl app service replicas notes web '${{ vars.web_replicas }}' --yes
piquelctl app service health http notes web 8080 --path /live --check-timeout 3 --yes
piquelctl app service health interval notes web 20 --yes
piquelctl app service health start-period notes web 60 --yes
piquelctl app service health clear notes web --yes
piquelctl app service replicas notes web 2 --deploy --yes
piquelctl app manifest notes

Every edit saves to the daemon’s internal manifest by default. --deploy saves and captures a deployment atomically, then waits for it; add --no-wait to return its receipt immediately. No local manifest file is read or rewritten. The server validates the complete result, so removing a mounted volume is rejected until its mounts are removed. Removing declarations retains Docker volume data. In a repository-backed application, pass self instead of a Git URL, as in source git notes web self or service add notes worker --git self, to build the manifest’s own repository. It builds the commit the manifest was read from, so it takes no --branch or --commit. app file add adds a read-only file, or replaces the one at the same target: --content sets its text, --from reads a local file now and saves its content inline, and --path names a file in the manifest’s repository, read by each deployment. Repository-backed applications are edited in Git instead. app file list shows each service’s saved files. app service secret add mounts a secret as a file under /run/secrets, or replaces the mount at the same target; its name may reference variables, so each environment can mount its own stored secret. app service secret remove unmounts it by target, and app service secret list shows each service’s saved mounts. Values and who may mount them are managed with app secret. Route edits preserve other routes and use the inspected generation to reject concurrent changes. route add and route redirect take an exact hostname or a wildcard such as *.tenants.notes.example.com, which serves every name one label below it; quote it for the shell. route redirect defaults to status 308 and appends the request path and query to the destination; use --status and --no-preserve-path to change that. Routes are private (tailnet only) unless added with --visibility public; route visibility changes an existing route, route add --name and route redirect --name name a new route, route name names an existing one (or removes its name without NAME), and env visibility caps every route of one environment at private (or lifts the cap with public) in the saved manifest. Deploy after saving to activate or remove routing. route list shows each deployed route with its name, environment, effective visibility, state, destination, whether piqueld manages its DNS records (manual, managed, pending or dns_conflict) and the records its hostname needs, followed by the cause while it is not ready. piquelctl status also shows the public and private listeners: the ingress mode (ports 80/443, or the Cloudflare Tunnel’s ID and whether it is connected), and the apps node’s name, state and tailnet addresses. It also counts the previews against their limits, installation-wide and for each application you may read, flags a count left over a lowered limit, and sums the CPU and memory limits of the previews’ deployed replicas. job set adds a job after the existing ones, or replaces the job with that name in place, keeping its timeout, volume access and whether it stops its service unless --timeout-seconds, --volume-access or --stop-service is given (300 seconds, read-only and off for a new job). --env KEY=VALUE and --secret NAME=TARGET, repeated for several, set the environment variables and secret files only the job’s container gets, added to its service’s; given, they replace the job’s own, which are otherwise kept. --clear-env and --clear-secrets remove them. read-only mounts the service’s volumes read-only; exclusive keeps writable volumes writable and stops every service mounting them until the rollout. --stop-service on stops the job’s own service before it runs, until the rollout. job move sets a job’s 1-based position in the run order. Jobs run in their saved order before each rollout; like routes, job edits preserve the other jobs and use the inspected generation. Each job inherits its referenced service’s depends_on: those services and their transitive dependencies start or update and pass health checks before the job runs. Configure dependencies with app service depends-on. Other services wait for all jobs to succeed; after a failed job, repair rolls its dependencies back and restarts the services the job stopped. Jobs of dependency services must come earlier in the saved order. export set lets the listed applications reach a service through a link, replacing who may, and whether previews may, if it was already exported; --previews lets their previews too. import add reaches another application’s exported service as <service>.<application>, in the environment named like each importing one unless --environment names one (it accepts ${{ vars.<name> }}); adding an import again replaces its environment. Like jobs, link edits preserve the others and use the inspected generation. env show lists each import of the environment and whether it connects, and the environments of readable applications importing its services.

app variable set sets a variable’s default, or with --env NAME its value in the environment named NAME, which must exist (an edit never declares a new environment, so an unknown name fails with environment_unknown; one that adds the first [spec.environments.<name>] block, like env visibility … private, declares every existing environment), or with --previews its value in every preview ([spec.previews.variables]). An environment’s or the previews’ value overrides the default, and a variable needs no default. true, false and integers keep their type and anything else is text; --string keeps text such as 3 as text. app variable unset removes the default, the environment’s value, or the previews’ value. Like routes, variable edits preserve the other variables and use the inspected generation. Settings that accept variables, such as replicas, limits, health check settings, environment values, commands and images, take a ${{ vars.<name> }} reference in place of a literal; quote it for the shell. app show lists the defaults and per-environment and preview values. Previews only exist for repository-backed applications, whose configuration is edited in their repository, so [spec.previews] is set there.

app service rollout replaces the service’s rollout block: an omitted --order derives the order from the mounts, and an omitted --monitor-seconds uses 30 seconds, so running it without flags restores the defaults. app plan lists each service’s effective order and monitor window, and warns when start-first is set on a service with a writable volume.

app plan --env NAME renders the manifest for that environment: its changes show rendered values, and it lists the value of every variable in scope, such as vars.domain and env.name. A reference without a value fails the plan with variable_value_missing. Without --env, the application’s only environment is used; with several, the plan compares the saved manifest as written and says that runtime actions are not planned. Likewise, a plan that must first resolve or build sources says that network, volume and service changes are left to the deployment; it still lists source resolution and declared jobs. Plans list, under Environments:, how saving the manifest would change the environments it declares.

Command, argument, job command, and depends-on arrays preserve individual elements; place command options before --. An empty array clears the setting. Optional limits and pinned commits use --clear; health checks use health clear. Nested Git/health settings require the corresponding source/check to be configured first. Use --help on any command for its values and options.

Git-backed applications are edited like any other: field edits and app apply change the saved manifest, which deployments of the connection’s branch deploy until a push changes the manifest there (see editing). app show lists the edits, and app repository reset discards them, saving the manifest last read from the repository again. Only app rename fails with repository_managed, since the fetched manifests must keep the name. Connecting points every environment at --branch (default main), and each environment’s branch then changes with env branch. Disconnecting retains the saved manifest, edits included, and also drops declared environments’ branch, commit and sync, returning them to the saved manifest:

piquelctl app repository connect notes https://example.com/infra.git infra/app.toml --yes
piquelctl app repository path notes corrected/app.toml --yes
piquelctl app repository sync notes poll --interval 120 --yes
piquelctl env branch notes production release --yes
piquelctl app repository reset notes --yes
piquelctl app repository disconnect notes --yes

Running commands

app exec runs a one-off command in a running task of a deployed service, for administrative work such as creating an invitation or opening a console:

piquelctl app exec piquel-fr auth -- auth-service invite create
piquelctl app exec piquel-fr db -i -- psql < dump.sql
piquelctl app exec piquel-fr auth -it -- /bin/sh
piquelctl preview exec piquel-fr feat/login auth -it -- /bin/sh

It runs in the application’s only environment; with several, --env ENV names one. preview exec runs it in a preview instead, addressed like preview logs. The command runs inside the task, so it shares the task’s image, environment, secrets, mounts and networks. It works over every transport, uses account authentication and needs no SSH access; the account (or token) needs apps:exec on the application. Output streams to stdout and stderr, and piquelctl exits with the command’s exit code. -i forwards standard input. -t allocates a terminal, implies -i, requires a terminal on standard input and puts it in raw mode, so Ctrl-C reaches the command. --timeout bounds the WebSocket handshake, not the session.

The service needs a running task; otherwise the command fails with service_not_running. The environment’s history records command_started and command_finished events with the account, task and exit code, never the command. Ending piquelctl early closes the stream, but the command may keep running in the task. The Terminal tab of an environment or preview in the dashboard runs commands the same way.

Mutation safety

app apply saves configuration only. app apply --deploy saves and deploys atomically. Both inspect the application identity and saved revision before confirmation; neither requires a runtime plan or Docker availability. Use app plan separately to inspect redacted changes and image resolution requirements. Planning may fail when Docker observation is unavailable.

Mutating commands require TTY confirmation unless --yes is supplied; env sync, env telemetry, app secret access, preview create and preview deploy do not ask. Apply, delete, rename, field edits, and env create, rename, branch and source accept --force independently of confirmation. Every explicit deployment creates a new snapshot, refreshes image references and supersedes pending work. Completed deployments retain their terminal state in history. Reconciliation retries the deployment snapshot using prepared digests. Unchanged healthy containers do not restart unnecessarily.

Deletion removes application configuration and all its history after runtime resources are absent. Environments’ named Docker volumes remain, which output reports as volumes_retained: true; previews’ volumes are removed with their data. The CLI waits for application absence because deletion also removes its operation record.

For apply, delete, rename, and field edits, the CLI automatically sends the revision it inspected before confirmation. Apply also sends the inspected application ID, or generation zero for create-only. --expected-generation N supplies an explicit revision for scripts. Conflicts stop the command without adopting the newer revision. --force skips these preconditions: forced apply targets whichever application currently has the name, or creates it if absent. Validation, resource ownership, and rename busy/name-collision checks still apply. app show reports intent and active target generations and separate runtime health.

The CLI retains one automatic transport retry, using the same command UUID in Idempotency-Key. SQLite receipts replay the original acceptance response for 24 hours across daemon restarts. Replays do not restart failed or superseded work; a separately invoked command gets a new UUID. This includes forced requests: a transport retry cannot overwrite changes accepted after the original command.

Rename checks the inspected generation and name availability. It rejects pending or running operations and deletion intent. It preserves identity, services, networks, and volumes. It saves without deployment unless --deploy is given. A changed name advances generation and records an event. Update metadata.name in your manifest file afterward; the CLI does not edit files automatically.

events reads one page, oldest first. --application ID optionally filters by stable application ID: application-wide events such as edits and renames, and the events of all its environments, including deleted ones. --environment ID filters by stable environment ID. Deleting an application removes its history. Use --cursor CURSOR for subsequent pages and --limit N (1–100, default 50). JSON includes the next cursor.

analytics summarizes deployment analytics, as the dashboard’s Analytics page does: deployments by the outcome of their latest attempt, the mean attempt duration, failed and retry attempts, action retries, mean durations per action phase, and the most frequent failure codes. It covers every application the caller holds events:read on, or, with --app, that application’s only environment; --env names one when it has several. --since DURATION starts the window that long ago (default: 30 days before its end) and --until DURATION ends it that long ago (default: now), so --since 14d --until 7d covers the week before last. A warning reports when the window reaches before retained history.

By default, apply with --deploy, delete, reconcile, deploy, and operation poll every 250 ms until a terminal state. Supersession returns immediately with exit code 0 and outcome: "superseded" in mutation command results (state: "superseded" on an operation record). It does not wait for the replacement to deploy. An observed failed attempt returns failure even if the controller will retry automatically. --no-wait returns immediately. For long image pulls, use a longer --timeout or return immediately and inspect the operation later. Pressing Ctrl-C ends the local command, including confirmation and network requests. It does not cancel an accepted server-side operation, which can still be inspected with piquelctl operation <id>.

The commonly useful exit codes are 0 for success or supersession, 1 for a general error, 2 for usage or input errors, 3 for conflicts, 4 for unavailable or timed out requests, 5 for a failed operation, and 130 when local operation waiting is interrupted. wait uses the same codes, except that supersession is a conflict (3): it waits for one deployment, so another one running is not success.

The dashboard provides application management forms and recent application logs. Remote registry management and advanced interactive CLI flows remain future work.

Use piquelctl setup-link on the daemon host to print the first-account setup link (--open also opens it in the default browser); only the Unix socket serves it. Use piquelctl login for passkey login through the browser, whoami to inspect the current account and its grants, and logout to revoke it. On the daemon host, root and the daemon’s own user need no login: without a token, they act over the Unix socket as the host operator, with admin on every application, and whoami prints host operator (uid 0). Run that way, piquelctl sign-in-link prints a one-time link (valid for 10 minutes) that signs a browser in as the host operator for 12 hours; it raises a security notification. piquelctl account lists accounts (and live host operator browser sessions), replaces their grants (access), edits (update, which also renames your saved login) and deletes (delete) them, and creates invitation (invite) and passkey enrollment (enroll) links; account passkey list|rename|remove manages passkeys, and account invitation list|revoke pending links. piquelctl token create|list|revoke manages API tokens for your account (--tailnet binds one to a tailnet user or tag); token list --user and token revoke --all [--user] list and revoke every session and token of an account. login accepts the same grant options to ask for a limited session; see ask for less access. piquelctl audit reads the audit trail, one line per request with the application or environment it addressed, filtered by --user (a username, or any account ID, including a deleted account’s), --credential, and --outcome; piquelctl audit verify checks its hash chain. When no administrator can sign in, sudo piquelctl recover-admin on the daemon host prints a one-time link that creates a new administrator; see recovering administrator access. Saved credentials are separate from profiles. PIQUELD_TOKEN supplies an automation token; --account selects a saved account. See signing in from the CLI for details.

app deploy and env deploy fetch a repository-backed environment’s manifest from its branch, then explicitly resolve image or Git build sources. They supersede pending work for the selected environment. Use --yes to skip interactive confirmation, --no-wait to return after acceptance, or a longer global --timeout for builds. --branch NAME or --commit SHA fetches a repository-backed manifest from another revision for this deployment only, without changing the environment’s branch; for example, to test a feature branch. Once the deployment finishes, they warn about anything it reported, such as manifest_connection_ignored when the fetched file’s spec.manifest names another repository URL or path. The server continues deployment if the CLI wait times out. app reconcile and env reconcile retry or repair the latest deployment snapshot and prepared target without refreshing sources.

Human output uses bold labels and color on terminals, application lists, and elapsed operation progress. Redirected output stays plain and NO_COLOR disables color. --quiet suppresses human results, information, and progress while preserving warnings, errors, and explicit JSON results. --noninteractive refuses prompts even on a terminal; destructive commands still need --yes.

Output is routed through channels configured once at startup:

RoleStreamQuiet behavior
Resultstdout, human or one typed JSON documentHuman hidden; JSON retained
Informationstderr, routine guidanceHidden
Warningstderr, incomplete results or actionable caveatsRetained
Progressstderr, task transitions and outcomesHidden
Promptstderr, an authorized interactive questionRetained
Errorstderr, final failure with context and hintsRetained

Every event is rendered and flushed as it occurs; output is not collected until the command ends. Terminal progress uses independently tracked task rows and permanent completion lines. Redirected progress prints meaningful transitions, without repeated identical updates. JSON remains a single result document, not a progress stream; with --json, stderr events are JSON lines. Paginated results can collect typed records before that document is emitted. If an environment’s status cannot be fetched during app list, app show or env list, its status is null (unavailable in human output) and a contextual warning is emitted immediately; other environments are still returned.

Dynamic human-readable values escape terminal control characters. Logs preserve newlines and tabs, but escape other controls. Build logs additionally use the shared ANSI/control cleanup before human rendering. JSON retains original values. Result, information, warning, and prompt write failures fail the command; progress and final error reporting are best effort.

Internally, command handlers emit typed Report values through one Console; they do not branch on --json or --quiet. Reports define their JSON type and stream human rendering through HumanWriter. Contextual errors and warnings share a borrowed diagnostic renderer, whose --json form is one stderr event; every error is one CliError with one JSON serialization. Task handles may be cloned across workers, but ordinary console writes remain serialized. Only explicit task completion prints an outcome; dropping the last unfinished handle clears its transient row without claiming the server-side operation was cancelled.

piquelctl app logs NAME_OR_ID [--service NAME] [--tail 200] [--since-seconds 3600] (or env logs APP [ENV] with the same options) reads a recent Docker snapshot with timestamps, service, task, and stream labels. --json returns the structured records and a truncation indicator.

Connection profiles

Select a named connection with --profile NAME or PIQUELD_PROFILE. On Linux and macOS, the CLI loads /etc/piqueld/profiles.toml, then $XDG_CONFIG_HOME/piqueld/profiles.toml (otherwise $HOME/.config/piqueld/profiles.toml). User profiles replace entire system profiles with the same name; fields are not merged. Other system profiles remain available. Discovery happens at runtime, so installed and development binaries use the same configuration.

--profiles-file PATH takes precedence over PIQUELD_PROFILES_FILE. Either explicit selection loads only that file, bypassing system and user discovery. Missing automatically discovered files are ignored. Explicitly selected missing files and all unreadable or malformed files are errors, with the file path. Surviving profiles are validated after replacement; invalid entries report their name and source file, even when they are not selected.

When neither --profile nor PIQUELD_PROFILE selects a profile, the CLI uses the one named by the optional top-level default, else an optional [profiles.default]. A later file’s default replaces an earlier one, and it may name a profile from either file. A default or an explicitly selected profile that does not exist is an error.

default = "testing"

[profiles.testing]
socket = "/tmp/piqueld-dev-run/piqueld.sock"
timeout = "2m"

[profiles.local-http]
url = "http://127.0.0.1:7845"

Profiles contain exactly one socket or url, plus an optional timeout. Precedence is explicit flags, then PIQUELD_SOCKET / PIQUELD_URL / PIQUELD_TIMEOUT, then profile settings, then built-in defaults. A transport override replaces the entire profile transport. Simultaneous environment socket and URL values are rejected unless an explicit transport overrides them. Authentication and credentials are not profile settings yet.

piquelctl profiles lists the effective profile names and endpoints in alphabetical order after file selection and replacement, marking the default. It does not contact a daemon or check endpoint reachability, and ignores --profile, PIQUELD_PROFILE, and connection overrides. Only configured profiles appear; the built-in local connection is not an additional profile.

NAME  ENDPOINT
dev   /tmp/piqueld-dev/piqueld.sock
prod  http://127.0.0.1:7845  (default)

piquelctl profiles --json returns {"profiles":[{"name":"dev","endpoint":"/tmp/piqueld-dev/piqueld.sock","default":false}]}. An empty list prints No profiles configured. in human mode, or {"profiles":[]} in JSON. --quiet suppresses human results, information, and progress, but preserves JSON, warnings, errors, and authorized prompts.

piquelctl builds list [--application ID] [--environment ID] [--cursor CURSOR] lists one page of build attempts and job runs, optionally of one application’s environments or of one environment. piquelctl builds logs ID [--before BYTE_OFFSET] reads the newest bounded output page, or an older page before the supplied cursor. It prints the cursor for loading older output when available. Both support --json.

Manually set secrets live in the application’s secret store and are write-only. Each lists the environments that may mount it, by default every environment, including ones created later, and no previews:

piquelctl app secret notes list
piquelctl app secret notes set database-password --file ./password --yes
printf '%s' 'new-value' | piquelctl app secret notes set stripe-live --stdin --environments production --yes
piquelctl app secret notes access stripe-live --environments production,staging --previews
piquelctl app secret notes access stripe-live --all-environments --no-previews
piquelctl app secret notes delete database-password --yes

Services mount these secrets with app service secret add.

list shows each secret’s version and who may mount it, naming environments. --environments a,b (names or IDs; --environments '' for none) or --all-environments sets the environments, and --previews or --no-previews whether previews may mount it; --all-environments never includes previews. Unset flags keep the current access. Environments are stored by ID, so renaming one keeps its access and deleting one removes it. Deploying, or app plan --env, for an environment or preview that mounts a secret it may not use fails with secret_access_denied.

Prefer protected files or a secure stdin producer over literal shell values in real use. --file stores the file exactly as it is. --stdin drops one trailing newline, so echo value | stores value. From a terminal, --stdin asks for one line and doesn’t echo it. --expected-generation pins a write to inspected metadata; otherwise the CLI reads the current generation before confirming. --json returns only metadata. Replacement creates a new version for a later Deploy and does not change running deployments. Deletion refuses secrets that any environment’s saved configuration or runnable deployment still uses.

Manifests can declare secrets that piqueld generates for each environment when a deployment first mounts them instead; see application manifests. Declare them with app generated-secret, like other saved settings:

piquelctl app generated-secret set notes database-password --bytes 32 --yes
piquelctl app generated-secret set notes signing-key --rsa-bits 3072 --yes
piquelctl app generated-secret list notes
piquelctl app generated-secret remove notes signing-key --yes

Changing or removing a declaration never changes values already generated. Generated values are listed, regenerated and deleted per environment. regenerate creates a new version for the next deployment, to rotate a value or replace one discarded by key recovery; running deployments keep theirs. delete removes an unused one:

piquelctl env secret notes --env staging list
piquelctl env secret notes --env staging regenerate database-password --yes
piquelctl env secret notes --env staging delete database-password --yes

env show lists each secret the environment mounts and where its value comes from: generated for the environment, the application store (with its version), missing, not allowed by the secret’s access list, or discarded by key recovery, which would fail the next deploy until replaced.

If the daemon’s secrets.key is lost and no backup exists, recover by discarding stored and generated values across ALL applications and environments:

piquelctl secrets recover-key --yes

The command requires confirmation (--yes for automation), refuses while the current key still works, and reports affected environment, secret and version counts. Running Docker services are left alone. app secret APP list and env secret APP list mark discarded values as unavailable; set replacement stored values using the same names, then Deploy explicitly, which also generates new values for discarded generated secrets.