Appearance
plexctl project
Synopsis
plexctl project is the operator surface for the Project aggregate. It wraps the typed /v1/projects operations (CreateProject, ListProjects, GetProject, PatchProject, DeleteProject) so an operator can manage Projects (create, list, get, update, delete) without hand-rolling curl calls.
A Project lives inside exactly one Domain and owns its mesh-IP sub-range reservation carved out of the parent Domain's mesh-CIDR. The CRUD surface documented here operates on the Project aggregate body itself; the parent Domain identifier is required at create time and is an optional server-side filter at list time. The sibling parent commands are documented in domain.md (the tenancy root the Project lives inside) and identity.md (the per-Domain identity surface). Two wire-shape decisions surface at the CLI layer: --display-name maps onto the JSON field name to keep the flag spelling consistent across domain create, project create, and group create; and --slug is immutable after creation — there is no --slug flag on update.
Invocation
text
plexctl project <subcommand> [flags]The five subcommands are enumerated below. Every subcommand is a private cobra constructor; flags are validated at parse time so an invalid UUID or a missing required flag surfaces as exit 2 instead of a wire round-trip.
plexctl project create
Creates a Project via POST /v1/projects. The handler marks --domain, --slug, and --display-name as required flags: the wire schema declares domain_id as required and the server's authorisation check pivots on the parent Domain UUID, so a missing --domain becomes an exit-2 cobra error rather than a 400 round-trip. The --display-name flag maps to the JSON field name — the rest of the CLI consistently uses --display-name for the human-readable label and --slug for the URL handle, so a Project-only --name would surprise operators who alias across aggregates.
plexctl project list
Lists Projects via GET /v1/projects. Unlike domain list --slug (which is a client-side filter applied after the page is fetched), project list --domain <uuid> is forwarded to the server as the domain_id query parameter — the wire ListProjectsParams natively supports it, so the server returns only the Projects in the named Domain. The opt-in --all flag walks every page until next_cursor is empty, capped at 100 000 items as defence-in-depth against a runaway server; --all and --cursor are cobra-mutually-exclusive .
plexctl project get
Returns a single Project by UUID. The positional argument must be a UUID — there is no slug fallback. domain get resolves slugs because ListDomains is bounded; for Projects the surface fans out across every Domain a caller can see, and a slug-to-id resolution would either need a mandatory --domain hint (changing the get UX) or scan every visible Project (a runaway-cost trap). Operators who only have a slug look up the id via plexctl project list --domain <id> first.
No
--domainflag onproject get. Unlikeplexctl identity get, which requires--domain(the wire endpoint is/v1/domains/{id}/identities/{principal_id}, nested under the Domain),plexctl project getaccepts only the positional UUID. The wire endpoint/v1/projects/{id}is Domain-scoped server-side via the Project's storeddomain_id, so the CLI does not need a--domainhint to disambiguate. Passing--domainhere is rejected at flag-parse time with cobra'sunknown flagerror (exit2); useplexctl project list --domain <id>if you need to enumerate Projects within a specific Domain first.
plexctl project update
PATCHes the mutable fields of a Project. Only the flags the operator explicitly set are forwarded in the request body, so the server's empty_patch rule never fires twice and a sparse update is genuinely sparse on the wire. --slug is intentionally absent because the OpenAPI ProjectPatchRequest schema documents the slug as immutable and the handler rejects any PATCH body carrying a slug key with 400 slug_immutable at decode time. --sub-range-cidr and --release-sub-range are cobra-mutually-exclusive: combining them would race the server's 422 sub_range_invalidates_allocation rule, so the CLI fails locally with exit 2 instead of round-tripping .
plexctl project delete
Deletes a Project via DELETE /v1/projects/{id}. The destructive operation is gated behind the root persistent --yes flag (not a local --force); the RunE refuses to call the API without the explicit confirmation, so a single typo of the project id cannot detonate without intent. On success the server returns 204 No Content and the command exits 0 with no body.
Flags
plexctl project create
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--domain | UUID | yes | — | Parent Domain UUID. Required by the wire schema (domain_id) and the server's authorisation pivot. |
--slug | string | yes | — | Kebab-case URL handle, unique within the Domain. Frozen after creation. |
--display-name | string | yes | — | Human-readable Project name. Maps to the JSON field name. |
--description | string | no | (empty) | Optional free-form description. Omitted from the request body when empty. |
--sub-range-cidr | string | no | (empty) | Optional canonical RFC 4632 sub-range reservation inside the parent Domain mesh-CIDR. Omitted from the request body when empty. |
plexctl project list
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--domain | UUID | no | — | Optional parent Domain filter. Forwarded to the server as the domain_id query parameter (server-side filter, not a client-side scan like domain list --slug). |
--limit | int | no | server default | Maximum items per page. 0 lets the server pick. |
--cursor | string | no | — | Continuation token from a previous call's next_cursor. Mutually exclusive with --all. |
--all | bool | no | false | Follow next_cursor until exhausted. Capped at 100 000 items as defence-in-depth against a runaway server. |
plexctl project get
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
<id> (positional) | UUID | yes | — | Project UUID. No slug fallback — use project list --domain <id> to resolve a slug first. |
plexctl project update
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
<id> (positional) | UUID | yes | — | Project UUID. |
--display-name | string | conditional | — | New human-readable Project name (maps to JSON field name). At least one update flag must be set. |
--description | string | conditional | — | New free-form description. Setting an empty string clears the field on the wire. At least one update flag must be set. |
--sub-range-cidr | string | conditional | — | New canonical RFC 4632 sub-range reservation. Mutually exclusive with --release-sub-range. |
--release-sub-range | bool | conditional | false | Clear the existing sub-range reservation. Mutually exclusive with --sub-range-cidr. |
A call with no update flag set fails locally with exit 2 and the message project update: at least one of --display-name, --description, --sub-range-cidr, --release-sub-range must be set .
plexctl project delete
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
<id> (positional) | UUID | yes | — | Project UUID. |
--yes (persistent) | bool | yes | false | Required confirmation for the destructive operation. Inherited from the root, not a local --force. |
Persistent flags inherited from root
--server, --profile, --token-file, --output, --yes (consumed by delete), --reveal-secrets. See ../plexctl.md for the canonical list and the resolution order between flags, profile, and the PLEXSPHERE_URL environment variable.
Output JSON schema reference
Every successful create / get / update returns a single ProjectResponse; list returns a ProjectList envelope carrying items[] and an opaque next_cursor token. Both schemas are pinned in the OpenAPI 3.1 spec at ../../../api/openapi/plexsphere-v1.yaml under the ProjectResponse and ProjectList definitions.
The text renderer projects the typed shape onto a tabwriter grid with the columns ID, DOMAIN, SLUG, DISPLAY_NAME, CREATED_AT. The DOMAIN column carries the parent Domain UUID (not the Project UUID) so an operator can pivot directly to plexctl domain get <domain-uuid> without round-tripping through JSON.
Exit codes
plexctl collapses every failure into the taxonomy below; the single source of truth is exitCodeFor in ../../../cmd/plexctl/app.go.
| Code | Reachable | Meaning |
|---|---|---|
0 | yes | The API returned the expected status (201 for create, 200 for list / get / update, 204 for delete). |
1 | yes | Runtime / API error: transport failure, unexpected status code, a malformed response body, or delete invoked without --yes (the destructive-confirmation refusal is a plain errors.New which falls through exitCodeFor's default branch, mirroring group delete / audit erase-identity). |
2 | yes | Flag-parse / misconfiguration: missing required flag, malformed UUID, mutually-exclusive combination (--all + --cursor, --sub-range-cidr + --release-sub-range), or an empty update patch. |
3 | yes | Missing or insecure credentials, or 401 Unauthorized from the API. |
4 | yes | Permission denied or resource not addressable: 403 Forbidden where the body's reason is not rebac_denied (e.g. token-audience mismatch), or 404 Not Found from the API. |
64 | no | Not reachable — project is fully wired and never returns *NotImplementedError. |
77 | yes | ReBAC denial: 403 Forbidden with body reason == "rebac_denied". Distinct from generic exit 4 so CI can branch on a ReBAC-specific authorization failure without parsing the response body. |
Examples
Create a Project under a Domain
shell
export PLEXSPHERE_URL="${PLEXSPHERE_URL:-https://localhost:8080}"
plexctl project create \
--server "${PLEXSPHERE_URL}" \
--domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
--slug billing-prod \
--display-name "Billing — Production" \
--description "Production billing services for the EMEA region." \
--sub-range-cidr 10.42.16.0/20List Projects filtered to one Domain (server-side --domain)
shell
plexctl project list \
--server "${PLEXSPHERE_URL}" \
--domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
--output jsonThe --domain flag is forwarded to the server as the domain_id query parameter; the server returns only Projects in that Domain. This contrasts with plexctl domain list --slug …, which fetches the page first and filters client-side because ListDomains does not expose a slug parameter.
Page through every Project in a Domain
shell
plexctl project list \
--server "${PLEXSPHERE_URL}" \
--domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
--all \
--output json--all walks next_cursor until empty and aborts with an exit-1 error after 100 000 accumulated items as a runaway-server guard. --all and --cursor are mutually exclusive at the cobra layer.
Sparse update (only --display-name)
shell
plexctl project update 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b1 \
--server "${PLEXSPHERE_URL}" \
--display-name "Billing — Production (EMEA)"Only the display-name field is forwarded in the PATCH body; the description and sub-range remain untouched on the server.
Update releasing the sub-range reservation
shell
plexctl project update 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b1 \
--server "${PLEXSPHERE_URL}" \
--release-sub-rangeCombining --release-sub-range with --sub-range-cidr is rejected locally by cobra:
shell
plexctl project update 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b1 \
--server "${PLEXSPHERE_URL}" \
--sub-range-cidr 10.42.32.0/20 \
--release-sub-range
# stderr: Error: if any flags in the group [sub-range-cidr release-sub-range] are set none of the others can be; [release-sub-range sub-range-cidr] were all set
echo $? # 2Delete a Project
shell
plexctl project delete 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b2 \
--server "${PLEXSPHERE_URL}" \
--yes
echo $? # 0The server returns 204 No Content and the command exits 0 with no body. Without --yes the command refuses to call the API and exits 1 with the error project delete: refusing without --yes (destructive operation).
ReBAC denial (exit 77)
shell
plexctl project delete 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b2 \
--server "${PLEXSPHERE_URL}" \
--yes
# stderr: plexctl: 403 Forbidden: rebac_denied (need project:delete on project/0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b2)
echo $? # 77The 403 carries reason == "rebac_denied" in the problem-details body; exitCodeFor routes it to exit 77 via the IsRebacDenied branch ahead of the generic *output.APIError mapping.
Cross-references
../../../api/openapi/plexsphere-v1.yaml—CreateProject,ListProjects,GetProject,PatchProject,DeleteProjectoperation definitions and theProjectResponse/ProjectListschemas.domain.md— sibling reference for the parent Domain aggregate the Project lives inside;plexctl domain get <domain-uuid>is the natural pivot from the DOMAIN column of the table view.identity.md— sibling reference for the per-Domain identity surface.../../../cmd/plexctl/commands/project.go— source of truth for the cobra command, flag wiring, the--allcap, the--sub-range-cidr↔--release-sub-rangemutual exclusion, and the table projection.