Skip to main content

crewship page

Manage Pages — workspace-scoped dashboards built from panels. A page holds no datasource and no credentials; a panel renders the last payload its declared producer pushed. See the Pages guide for what a page is (and is not), the freshness contract, and the permission model these commands enforce.
Routes are workspace-unscoped (/api/v1/pages/...); the CLI supplies the workspace from your current context the same way it does for crewship saved-view and crewship routine. Every subcommand honours the global --format table|json|yaml|ndjson|quiet. Machine output is the server’s own document passed through unmodified rather than re-encoded, so a field this build has never heard of is not lost on the way out. --format quiet prints one key per line — the slug, the link id, the webhook id, the action id, the version seq — so a listing can be piped into the command that consumes it. A listing with nothing in it prints nothing under quiet, rather than the sentence the human format would use: an empty pipe is empty, and a line saying so would be read as a key. page export is the exception in the other direction: it prints YAML unless you ask for --format json.

Subcommands


crewship page list

Backed by GET /api/v1/pages. Returns the pages you may see — a page you have no read grant for and no crew membership on any of its panels for is not in the list. list takes no flags of its own. Columns are SLUG NAME PANELS STATE OWNER REACH LAST DATA, where PANELS counts every panel on the page including the ones sealed to you, and REACH says how you reach the page — owner, role, crew:<slug>, panel_crew:<slug>, folder:<slug>, grant, comma-joined when there is more than one path. folder:<slug> means the folder the page is filed in is shared with you, one of your crews, or the whole workspace. A page whose only path is grant is one somebody shared with you. --format quiet prints one slug per line, which is the form the next command in a pipe wants.

crewship page get <slug>

Backed by GET /api/v1/pages/{slug}. Prints the page definition and every panel you may see. A panel whose owning crew you are not in, and for which you hold no grant, arrives as a sealed placeholder — its id, its width, its tab and its owning crew’s name, and nothing else — so the page has the same shape for every reader. In the human output that is one line:
In --format json the panel carries "sealed": true. Key on that field being present rather than on other fields being absent: a serialisation bug and a permission decision are opposite failures, and only one of them is safe.

crewship page create --file <file>

--file is a Layer-1 page definition — human-authored YAML, apiVersion: crewship/v1, kind: Page:
Every panel this page needs is declared here, and every example further down pushes to one of them — so the commands below can be pasted in order against a page you actually created. The CLI parses this file and sends the parsed spec as JSON, not the YAML text — the server validates a structured spec, not an opaque string. sla: 30s is YAML sugar; on the wire it travels as sla_seconds (an integer). Every panel needs sla; a panel without one does not validate — there is no default that means “never mind” (§4). owner names the crew whose membership gates who can see the panel; it is the permission anchor, not decoration. producer names the routine, script, agent or webhook allowed to push data into it — see page set below. Three optional keys shape the page rather than its permissions: span: is 1–12 and defaults to 12, the full width of the grid. A page holds at most 24 panels and 256 KiB of spec. Remember that page update --file replaces the spec: omit any of these on a re-apply and they are deleted.

--owner crew/<slug> — whose page it is

Without it, the page belongs to whoever ran the command. That is right for a personal page and wrong for a team’s board, because page ownership is what decides who may hand-write a script- or webhook-produced panel — and ownership of a crew-owned page counts every member of that crew. Hand the page to crew/ops and anyone in ops can correct a panel by hand, without a grant and without stopping the producer that normally writes it. (A workspace admin can do that on any page, owned or not.) Note the two different owners. A panel’s owner: decides who may see it. The page’s owner — and a workspace admin — may hand-write a script- or webhook-produced panel and edit the spec. A panel produced by a routine or an agent is writable only by that producer, or by a subject holding an explicit produce grant, no matter who owns the page. The two owners are set in different places on purpose: the panel’s is part of the document, the page’s is not. --owner takes crew/<slug> and nothing else — not ops, not user/<id>. Your own id is already the default, and the crew has to exist in this workspace or the create is refused naming it. Ownership is a create-time decision: page update re-applies a spec and never moves it, because a re-apply that silently transferred a page would be a permission change nobody asked for. There is no transfer command at all — ownership moves only when the owning user is erased from the workspace, and then only to a crew. page get and page list both show the owner, so what you set is visible without reading the database.

--icon and --color — the page’s own avatar

A page wears an icon and a colour the way a crew, an agent and a folder do, so the list on the left of /pages can tell eight pages apart by something other than their freshness glyph. The state (fresh, stale, failed, never produced) keeps its own glyph beside the avatar, always — the avatar never stands in for it, and the STATUS filter is unaffected by it. Like --owner, the avatar is not part of the document: it is stored on the page row, not in the spec, so a re-apply of the file leaves it alone, a page rollback does not restore an older one, and an export bundle does not carry it. page get and page list both return icon and color (empty strings for none, never a missing field). A panel may also declare the sensor half — a wake: threshold that opens an issue on a crew when the pushed payload crosses it, and on_failure: for when it stops arriving at all:
writes: has to name a panel on this page — a gate pointing at a panel that is not there is refused when the page is saved. It also does not grant anything: the woken agent still needs produce authority on that panel, which on the container door means the panel declares producer: agent/<its slug> or a human granted it produce. A gate whose target declares producer: routine/… is answered by running that routine, not by the woken agent writing into it. The CLI sends both verbatim and the server parses them: when is checked against the panel’s own schema, and a predicate that panel could never satisfy is refused here rather than accepted and silently never matched. See Pages → wake gates for the grammar and page get plus crewship automation list for seeing what a gate became. A panel produced by a routine may also declare refresh: — on:wake or on:panels-changed, and nothing else. It is the event that runs that panel’s producer, so the analysis is already on the page when a human arrives instead of starting when they get there:
The CLI refuses a value outside those two before it sends anything, and the server refuses a refresh: whose producer is not a routine/ (it cannot run a script, call a webhook producer, or dispatch an agent), an on:wake on a page that declares no gate, and an on:wake on a panel that declares its own gate — which is a loop. Like a gate, it compiles to a row crewship automation list shows. See Pages → refresh. Backed by POST /api/v1/pages.

crewship page update <slug>

Backed by PATCH /api/v1/pages/{slug}.
With --file, this replaces the spec, it does not merge into it: a panel field left out of the file is deleted, including tab:, icon:, public:, actions:, wake:, on_failure: and refresh:. Panels are reconciled by id, so a panel that survives the edit keeps its payload ring.
Neither read command emits a document you can feed straight back. page get --format json returns the READ shape — sla_seconds as an integer, plus state, data, provenance and an authored flag — and update --file refuses it (field authored not found in type pages.Document). page export returns a bundle, a different format again, and it deliberately drops wake:, on_failure:, actions:, refresh: and public:.So keep the document you authored. If you have lost it, rebuild it by hand from page get --format json: wrap it in apiVersion / kind / metadata / spec.panels, turn sla_seconds: 300 back into sla: 300s, and drop everything the server attached. Check the panels that carry public: true survive the trip — that flag is the one whose loss is silent.
If the document declares a metadata.slug different from the slug you passed, the CLI refuses before sending: a page’s slug is its address. --owner is create-only and update never sends it, so a re-apply never moves ownership. Every save of the document — through this command, the in-app editor, or an agent with a write grant — becomes a new row in the page’s version history. See page rollback to undo one. An update that changes only --icon or --color is not a save of the document: it records no version and leaves the page’s updated_at where it was. Replacing the panels requires that you can read every panel in both the current and the submitted definition. A write grant alone does not give you that: a panel owned by a crew you are not in is sealed to you, and a document that omits it would delete it. The server answers a partial reader with a neutral 403 (exit 4) rather than a truncated save. If somebody else saved the live definition between your read and your write, the update is refused with 409 (exit 5) and the newer document is left intact — re-read it and apply your change again.

crewship page delete <slug>

Backed by DELETE /api/v1/pages/{slug}.

crewship page set

The single write path for panel data. Everything that produces a number, status, or table on a page — a cron job, a script on a host you control, a routine step — ends here.
Backed by PUT /api/v1/pages/{slug}/panels/{id}/data. page set authenticates with your session or CLI token the way any other crewship command does, and a producer script always calls the same command wherever it runs.
A producer running inside a crew container does not use this command, and could not: there is no crewship binary in the sandbox image. It pushes to its sidecar — PUT http://localhost:9119/pages/{page}/{panel}, body = the payload, ?state=failed when it ran and could not measure — which attaches the identity from the agent’s own token and forwards to PUT /api/v1/internal/pages/{page}/data. The credential stays in the sidecar and never enters the agent process:
The fd-3 form is deliberate: a bearer token passed as -H is visible in the container’s process table.Note the panel: rozbor, not sluzby. rozbor declares producer: agent/riley, and the curl above runs inside riley’s container. sluzby declares producer: script/watch-services.sh, and this door would refuse it — which is the rule stated immediately below, so an example that pushed there would be teaching the opposite of the paragraph it sits in.Declare such a panel producer: agent/<slug>, not producer: script/…. A script producer means “a human’s CLI token pushes this”, and the unattended path admits only the declared routine/ or agent/. A container writing a script-declared panel is refused with 403 unless a human issued it a produce grant. See Pushing from inside a container.
The server, not the script, attaches provenance (producer, run_id, produced_at) and the freshness clock. A payload the caller cannot prove they own — the caller is not the panel’s declared producer — is refused with 403 (exit 4) when the caller can otherwise reach the page, and with 404 (exit 3) when they cannot, so the refusal never confirms a page exists to someone who may not see it. Either way the refusal itself writes a journal entry and notifies the page owner (§7.1b rule 3): treat it as a signal to investigate, not noise to retry past.

A working producer script

The panel above is metric.v1 ({value, unit?, delta?, target?, sparkline?[]}). A cron job or a step inside a routine can push it with nothing more than jq and the CLI:
A status.v1 panel ({items[{name, state: ok|warning|critical, label}]}) pushes a list rather than a number:
A table.v1 panel takes keyed row objects matching its declared columns, not positional arrays — this is the canonical shape the schema validates against:

Limits that turn into responses, not silent drops

The 2 s floor is the one a --loop 1 producer meets first, and it is not a token bucket: it is enforced inside the write itself, so it holds across replicas and across all four doors. The CLI prints the scope and the wait — this panel is being pushed faster than its rate limit allows — retry in 2s — and exits 6, so a loop can sleep exactly that long rather than guess.

crewship page actions

List the actions a panel declares.
Backed by GET /api/v1/pages/{slug}/panels/{id}/actions. The list comes from the page’s stored spec — the same list the server resolves a click against — so what you see is exactly what can be dispatched. A panel you may not see answers 404, the same answer an unknown panel gives, so an action list is never an existence oracle for somebody else’s crew. Only call actions reach the server. A link navigates to an internal entity (issue, run, page, agent) and the renderer builds the address — there is no URL field anywhere in the schema and there will not be one. A toggle is client-side panel state. A custom action resolves to a handler compiled into the web client. --format quiet prints one action id per line — column 0 is the argument page action takes, so the two compose:
A panel that declares no actions prints nothing at all under quiet and exits 0. Do not read the empty output as a failure — a panel with no actions and a panel you may not see are told apart by the exit code, not by the text.

crewship page action

Dispatch one of a panel’s declared actions.
Backed by POST /api/v1/pages/{slug}/panels/{id}/actions/{actionId}, or with --publication by POST /api/v1/pages/{slug}/application/actions/{panelId}/{actionId}.

You cannot name a routine here, and that is the design

There is no --routine flag, and the request body carries the collected inputs and optional intent/publication preconditions. The server resolves the action id against the page’s stored spec and dispatches the routine named there. A compromised client, an injected narrative panel, or an agent cannot choose what runs, because the wire format has no field for it. The allow-list is not a check the server remembers to perform — it is the only path that exists. Inputs are validated server-side against the action’s own declaration. An input the action did not declare is refused, not passed through; a required input that is missing is refused before anything runs; a fixed param the page author set is not yours to override.

It returns when the run is queued, not when it finishes

The answer is 202 with a pending id. Nothing waits for the run — a page button on a ten-minute routine must not hold a connection. Watch the run itself on the page, in the activity feed, or with crewship routine runs.

Retrying is safe, replaying with different inputs is not

Every dispatch carries an Idempotency-Key, generated locally, one per invocation. Pass --idempotency-key to pin it so a retry from a shell loop or a CI step resolves to the original dispatch instead of starting a second run. Reusing a key with different inputs is refused with 409 rather than silently deduped onto the first run — a replayed key that quietly returned somebody else’s result would tell you a click succeeded that never happened.

Who may dispatch

Two halves, and both must hold:
  1. You can see the panel — membership of its owning crew, or a workspace admin role. A caller who would get a sealed placeholder gets 404, not 403: the action does not exist for them, and a 403 would confirm it exists for someone else.
  2. You hold what the routine requires — MANAGER+, and the routine must be active. A page button is never a cheaper way to run a routine than the routine’s own surface.
Every dispatch is journalled as page.action.dispatched, carrying who clicked, which action, and which routine the server resolved it to.

Declaring an action

Actions are declared in the page document, by a human editing the page. An agent can write a panel’s data; it can never author the button underneath it.
Refused at save, every time:
  • an action kind outside call | link | toggle | custom, or none at all;
  • a call that names no routine, or names one that does not exist here;
  • a link carrying anything URL-shaped instead of an entity id;
  • a toggle targeting a panel that is not on the page;
  • two actions on one page sharing an id;
  • an input named after a fixed param, or an input collecting a secret;
  • actions on a narrative.v1 panel — a panel that renders agent-written prose and can also trigger an action is refused in this release.

crewship page grant <slug>

Grants layer on top of a panel’s crew-based visibility — they widen who may reach the page, never who may reach a crew’s data (§7.1 rule 3, §7.1b).
Only a human can run this command successfully. An agent holding write on the page can rebuild its layout freely but cannot issue a grant — not even to another agent in its own crew — by any route, including this one (§7.1b rule 1).
Every grant change is journalled (page.grant_added), so who granted what to whom is always answerable later. Backed by the page’s grants endpoint (GET/PUT/DELETE …/grants).

crewship page revoke <slug>

Removes a grant, journalled as page.grant_removed. revoke takes the same subject flags as grant — --user, --crew, --agent, --workspace — plus an optional --level. Omit the level and every level that subject holds is withdrawn at once.
A grant can also stop working without being revoked. It is live only while the human who issued it still has the reach they granted from: leave the workspace, lose ADMIN, or be removed from the page’s owning crew, and every grant you issued narrows with you. The row stays — page grants shows it as inert, with the reason — so you can see what would come back if that person’s access were restored. Revoking is how you remove a grant; this is how one stops being borrowed authority nobody is accountable for any more.

crewship page grants <slug>

Lists every grant on the page: subject type, subject, level, and (for produce) which panels it covers.

crewship page access <slug> --me

Prints the paths by which you reach the page, in the API’s own words — owner, role, crew:<slug>, panel_crew:<slug>, folder:<slug>, grant — the same words page list prints in its REACH column. When the page is in a folder, the folder’s slug and its sharing label (none, people, crews, people_and_crews, workspace) are printed too: the label, never the names. Anyone who can open the page may ask; a page you cannot reach is a 404. Backed by the page’s own-access endpoint (GET …/access/me).

crewship page folder

A folder is a named group of pages owned by one crew, with an icon from the crew icon set and a colour from the crew palette — the same picker a crew uses. One level, a page in at most one folder; a page outside any folder is unfiled. A folder shows you only the pages in it you reach. A folder can be shared — with a person, a crew, or everyone in the workspace — and what it is shared with applies to every page in it right now: filing a page in a shared folder shares it, taking it out unshares it. Can view opens the pages; can edit also renames the folder, edits the pages inside and takes pages out of it. A panel owned by another crew stays sealed whatever the folder says, and a folder is never shared with an agent. Only the owning crew’s managers and workspace admins see or change a folder’s sharing; everyone else sees a label (none, people, crews, people_and_crews, workspace) in folder list and folder show, and their own access in page access --me. Every folder command is backed by the folder endpoints.

crewship page folder list

The folders you can see — the ones you reach a page in, your crews’ own, or all of them as an admin — with PAGES counting the pages in each that you reach. --format quiet prints one slug per line.

crewship page folder create <slug>

A workspace admin, or a MANAGER (or higher) who belongs to the owning crew, may create it; the folder is the crew’s from birth.

crewship page folder show <slug>

The folder and the pages in it you reach, each with its REACH column. A folder you reach no page in — and have no standing on as its crew or an admin — is a 404, indistinguishable from one that does not exist.

crewship page folder update <slug>

Only the flags you pass are sent. The slug and the owning crew are fixed at creation. An admin, the owning crew’s MANAGER (or higher), or somebody the folder’s sharing lets edit it.

crewship page folder delete <slug>

Only an empty folder: one still holding a page — including one you cannot see — is refused with 409, and the refusal does not say how many. Move the pages out first (crewship page move <page> --unfiled).

crewship page folder add <folder> <page>

The same as crewship page move fleet-201 --folder ops-board. See page move for the two authorities it needs and the fences it carries.

crewship page folder remove <folder> <page>

The same as crewship page move fleet-201 --unfiled, naming the folder explicitly. The page’s owner, a workspace admin, or somebody the folder’s sharing lets edit it may — the last on purpose: taking a page out withdraws the access everyone else inherited from the folder. Taking your own page back never waits for anyone.

crewship page folder acl <slug>

Who the folder is shared with: each subject, can view or can view and edit, who set it and when, and the folder’s acl_version. Only a workspace admin or a MANAGER (or higher) of the owning crew may read this; everyone else sees the sharing label in folder show and their own access in page access --me. The owning crew and admins are not listed: they hold the folder without an entry.

crewship page folder share <slug> <subject>

Exactly one of the two. A subject is user:<email or id>, crew:<slug>, or the word workspace for every current member. Never an agent: an agent reaches a page only by page grant --agent on that page, and the command refuses agent: before any request. Sharing applies to every page in the folder now or later. Can edit never opens a panel owned by another crew, moves a page elsewhere, deletes the folder or changes its sharing — and it can take pages out, which withdraws the access everyone else inherited. Only a workspace admin or a MANAGER (or higher) of the owning crew may run this. The entry belongs to the folder and stays when whoever set it leaves. Repeating the command is the same state, not a second entry.

crewship page folder unshare <slug> <subject>

Removes one entry, with the same subject spelling as share. A user who has since left is removed by the id or email folder acl prints. Only a workspace admin or a MANAGER (or higher) of the owning crew may run this. Sharing changes are journalled as page.folder_acl_changed.

crewship page move <page>

Exactly one of the two. Moving a page into a shared folder shares it with whoever the folder is shared with; moving it out unshares it. Who may. Filing needs both halves of the authority in one caller: you initiate as the page’s owner or a workspace admin, and the folder accepts you as an admin, a MANAGER (or higher) of its owning crew, or somebody its sharing lets edit it. Lacking either, the refusal names who can; can edit alone never takes somebody else’s page. Unfiling is the page owner’s, an admin’s, or a can edit holder’s — taking your own page back never waits for anyone. Several pages. With more than one page and --folder, the command sends one request: every page is checked on its own, one refusal moves nothing, and the error names the page. The fences. The command reads each page’s pages_version and the folder’s acl_version right before it writes and sends exactly what it read. A 409 therefore means somebody else moved a page, or changed the folder’s sharing, between that read and the write; the error prints the current pair — look again and re-run. There is deliberately no flag to type a fence by hand: a fence you typed says nothing about what you saw. Moves and removals are journalled as page.folder_membership_changed; the folder’s own changes as page.folder_changed.

crewship page access

One command, two questions, one per invocation. With a slug it answers who reaches this page — every user, crew and agent, each with the paths that carry it. With --subject it answers the reverse: which pages does this subject reach, and how. Both are read-only renderings of what the server already enforces; no grant is issued or implied, and an inert grant is not a path. A path is one of the server’s own words, printed verbatim:
Reading a page’s access takes the same right as reading its grants: the page’s owner, or a workspace ADMIN/OWNER. Asking about a subject is an administrator’s, except that anyone may ask about themselves (--subject user:<your email>). Every other caller is refused with the server’s sentence and a non-zero exit. --format json prints the server’s document unchanged — the page (or the subject), the rows with their paths, and next_cursor when more follow.

crewship page export <slug>

Produces a portable bundle: workspace-specific ids are stripped, and every external reference the page needs (crews, producers) is declared so an importer can see what it must bind before installing. This is the same export/import mechanism crewship routine export uses for the marketplace — a “page template” is not a separate object, it is an exported page spec.
Export carries the page spec only, not panel data. Data is state that belongs to the install it came from; moving it between workspaces would mean shipping numbers with no producer behind them.
The bundle carries a panel’s structure — id, schema, owner, producer, SLA and span. It does not yet carry wake: gates or on_failure:, so an imported page arrives unmonitored and the gates have to be added again in this workspace. That is deliberate rather than silent: a gate names a crew, and a crew reference has to be bindable (--bind) before it can be carried, or an import would install a gate pointing at a crew that does not exist here and quietly wake nobody. Until then the bundle omits what it cannot bind.

crewship page import [bundle.yaml]

Import either binds every declared reference and creates the page, or refuses and names which reference it could not resolve — it does not create a page full of dead panels pointing at a producer that does not exist locally. --bind is repeatable, once per reference, and is deliberately not comma-separated: both halves are slugs, and a slug may plausibly contain a comma where a flag may simply be repeated. Binding one reference twice is refused rather than resolved last-wins.

crewship page rollback <slug>

Rolls back the page structure — which panels exist, their layout, their owners and producers. It never resurrects old panel data: a panel a rollback brings back renders dimmed, in a “waiting for first data” state, even if rows for it survive in the payload ring. The ring is cleared for the same reason for a panel whose schema, producer or owning crew the rollback changed — the footer would otherwise credit a producer that did not produce the number, or show a payload to a crew the restored spec never admitted. Showing an old payload as current after a rollback is exactly the dishonesty the freshness contract exists to prevent. Up to the last 50 versions are kept. A rollback replaces the whole definition, so it has the same visibility rule as page update --file: you must be able to read every panel in both the current and the archived definition. A partial reader gets a neutral 403 (exit 4). If the live definition changed after you read it, the rollback is refused with 409 (exit 5) and the newer document stays; reload before restoring.

crewship page versions <slug>

Lists the page’s structural history — who changed the layout, when, and what version number to hand page rollback. Up to the last 50 versions are kept. Panel data is not versioned here; a version is a record of what the page was shaped like, not of what it showed.

crewship page publish <slug>

Publish a page to someone outside the workspace, behind an expiring link.
The URL is printed once. The token is stored as a SHA-256 digest, so nothing can show it again — not this command, not page links, not the database. Lose it and you mint a new one. Publishing exposes only the panels explicitly marked public, never the whole page. See Public pages.
Lists a page’s public links and says which of them still work — expired, password-protected, and what each one exposes. It cannot show you a link again; see page publish above for why.

crewship page unpublish <slug>

Withdraws one link without touching the others. Several links per page is the intended shape — revoking the one you sent a supplier does not break the one on the wall display.

crewship page webhook

Let a producer that cannot run this binary write one panel: a cron on someone else’s box, a Zapier step, a PLC gateway, a GitHub Action. Anything that can run the CLI should use page set instead — one write path, provenance attached server-side, and no credential to leak.
create prints the URL once and nothing can show it again: the token is stored as a SHA-256 digest, the same at-rest shape pipeline webhooks use. Copy it into the sender’s secret store, or mint a second one — several tokens per panel is the intended shape, so revoking the PLC’s does not break the GitHub Action’s. The sender POSTs the panel’s payload as the body, with no envelope around it:
A producer that ran and failed says so on the query string — ?state=failed — with whatever payload it has. fresh and stale are the server’s arithmetic and are not a sender’s to claim, and neither is produced_at: provenance is attached server-side, so a body carrying a timestamp or a producer name is refused rather than believed.

What the token can and cannot do

A webhook is a produce grant in a different coat, and it obeys every rule that grant does. A token whose panel is deleted from the page spec dies with it, the same way the panel’s payload ring does.

Flags

webhook list never shows a token value — the column holds a digest, so there is nothing to show. A revoked token stays in the listing on purpose: “was it used after we pulled it” is the question an incident asks, and a deleted row cannot answer it.

Exit codes

Standard CLI exit codes. What a producer script needs to branch on: Refused locally, before anything is sent (always exit 2): a page document that does not validate, a <page>/<panel> address with no slash, an empty or non-JSON --data, an --owner that is not crew/<slug>, a missing --file/--panel/--id, a --to that is not positive, an --input that is not k=v, a malformed or repeated --bind, a bundle whose format is neither crewship-page-bundle/v1 nor crewship-page-bundle/v2, and a --file whose metadata.slug disagrees with the slug you passed to update. Everything else is the server’s answer — including an unknown slug and an unknown --level, both of which are checked there and not here.

Custom React projects (experimental)

The opt-in Pages application profile uses React, TypeScript and Vite. Projects are source drafts; saving/importing one does not execute or publish its code. The operator must configure protected source storage, a pinned build image and a separate-site runtime origin. The current profile accepts its fixed dependency set and lockfile, with custom React and CSS. It does not install arbitrary packages. Every page project subcommand prints the server’s document — JSON unless --format yaml or ndjson asks otherwise; there is no table form — except init and pack, which print source YAML. The lifecycle, in order:
Source limits are 256 files, 512 KiB/file and 2 MiB total decoded content. Symlinks, environment files and paths outside the project are refused. Who may do what. Every page project command that reads or writes source (get, history, restore, set, build, preview, check, review, fsck) and page export require Page edit permission, not just permission to view its panels. The commands that return or replace a whole definition additionally require that you can read every panel in it: get (without --source-only), get --revision, export, set and restore. A write grant does not grant visibility of another crew’s panels, and a document with a panel missing would delete it on save, so a partial reader is answered with a neutral 403 (exit 4) rather than a truncated document. set and restore apply the rule to the live definition, the existing draft and the proposed definition alike; a refused save leaves the draft revision unchanged. get --source-only omits the definition and is not gated this way. review is not refused either: it withholds the panels you cannot read from both documents and reports excluded_panels and withheld_changed instead (see below). Publishing, rolling back and withdrawing require Page ownership or workspace administration. application, action-status and panel-history are reader operations: they need the page to be visible to you and, for the last two, the panel too.

crewship page project init

Prints the supported React/TypeScript starter as one source YAML document, or with --dir writes its files into a new working directory. An existing directory is never overwritten. --format json prints the same document as JSON. Nothing is sent to the server.

crewship page project pack <directory>

Prints a working directory as one portable source YAML, the document set takes. .git, node_modules and dist are skipped. Nothing is sent to the server. pack takes no flags of its own.

crewship page project unpack <directory>

Extracts a source YAML/JSON document into a new working directory; an existing one is never overwritten. Nothing is sent to the server.

crewship page project get <slug>

Backed by GET /api/v1/pages/{slug}/project, or with --revision by GET /api/v1/pages/{slug}/project/history/{revision}; --source-only appends /source to either. Prints the draft’s revision, its project (the source files) and its definition (the authored Page document the draft would make live). --source-only omits the definition, which is the form to use when you want to read code without requesting the complete panel declaration.

crewship page project set <slug>

Backed by PUT /api/v1/pages/{slug}/project. Saves a source document against the draft revision you expect to be current. The reply carries the new revision and a git_commit: every save is a Git checkpoint.

crewship page project history <slug>

Backed by GET /api/v1/pages/{slug}/project/history. Lists audited source revisions — who saved each one, when, and its Git checkpoint — at most 50 per call.

crewship page project restore <slug>

Backed by POST /api/v1/pages/{slug}/project/restore. Restores a historical source revision as a new draft, creating a new revision and Git checkpoint. It never touches the live application; publishing the restored draft is a separate, reviewed step.

crewship page project build <slug>

Backed by POST /api/v1/pages/{slug}/project/build. Starts an isolated preview build of the given source revision in the pinned build image. Returns a job receipt, not a ready preview; preview reads its state. A busy compiler returns 429 without a waiting queue.

crewship page project preview <slug>

Backed by GET /api/v1/pages/{slug}/project/preview. Prints the latest build’s state (running, ready, failed, interrupted), its diagnostics, and the artifact once it is ready. preview takes no flags of its own.

crewship page project check <slug>

Backed by POST /api/v1/pages/{slug}/project/check. Verifies a built candidate: stored source and artifact integrity, the compiler result, and that the routines and crews the definition binds to still resolve. A clean check is not a review — it still requires browser and code review.

crewship page project review <slug>

Backed by GET /api/v1/pages/{slug}/project/review. Prints the snapshot a publication is fenced against: the candidate draft revision and its build, the live publication and its retained source, the routine definitions each call action would run, and machine-readable blockers explaining why publishing is refused. A baseline it cannot read back reports source_available: false with a reason — that is a missing comparison, not agreement — and a routine with no recorded digest is unknown, never unchanged. Before the draft has been built the snapshot carries build_missing; before there is a draft at all, no_candidate. --publication <version> asks the same question about a retained version instead of the draft: the candidate becomes that publication’s archived source and its routines become the ones its archived definition declares, which is what a rollback is fenced on. review also answers whether the comparison it just printed is the whole change. Panels belonging to crews you are not in are withheld from it, and excluded_panels says how many; withheld_changed says whether any of them actually differs between the live definition and the candidate. When it does, publish and rollback are refused with 403 and blockers carries withheld_change.

crewship page project publish <slug>

Backed by POST /api/v1/pages/{slug}/project/publish. Publishes the exact candidate you reviewed: the build, the source revision, and the Page definition the draft carries. Use the actual build ID, source revision and publication version returned by the server. --expected-publication 0 creates the first publication. Exact retries return the original receipt. Publication requires Page owner/workspace admin authority. An ordinary write grant does not grant permission to publish executable code for readers. The fence. publish and rollback send two fence values. --expected-definition-digest is the sha256 of the Page definition that was reviewed, and --expected-routine-digest routine=sha256 (repeatable) is the routine definition behind each call action. Pass either flag and that half is sent verbatim. Omit them and the CLI reads project review itself immediately before publishing — rollback asks it for the retained version it is restoring — uses the digests it returns, and prints each one to stderr as it sends the request. That saves copying hex by hand, and it leaves a record of what the publication was fenced on. It is not a review step: the command does not pause, and nobody sees those digests before they are sent. To review first, run crewship page project review <slug>, read it, and pass the values back with the flags. If the live definition or a routine script moves between the snapshot and the write, the server refuses with 409 and names which one. Withheld panels. --reviewed-code attests that the whole change was reviewed, and that attestation cannot be made by a publisher who cannot read all of it — the rollback path included, since restoring a version replaces the live definition too. When review reports withheld_changed, the server refuses with 403. Publishing the Page is still yours to do; this particular claim is not yours to make, so there is no flag that waives it, unlike the baseline acknowledgement below. Ask a workspace administrator, or a member of the crew that owns the withheld part, to publish instead. The CLI prints the refusal ahead of the request, and says how many panels are withheld and nothing about what they are. Unavailable baseline. --acknowledge-unavailable-baseline is separate from the digests and is never filled in for you. When the live publication’s retained source cannot be read back — history compacted, a checkpoint lost — there is nothing to compare the candidate against, and the server refuses the publication with 409 until somebody says so explicitly. The CLI prints what is missing and what acknowledging it would mean, and then sends the request as you gave it; passing the flag records that nobody made that comparison. It proves nothing about the candidate, which is verified in full either way, and the publication’s checks keep the answer as baseline_source. An initial publication has no baseline and needs no acknowledgement.

crewship page project rollback <slug>

Backed by POST /api/v1/pages/{slug}/project/publish with rollback_version. Creates a new publication of an earlier artifact and Page definition, keeping the source draft unchanged. It does not undo work already performed by routines. The fence, the withheld-panel rule and the baseline acknowledgement are the same as publish’s, computed against the retained version being restored. Publication metadata is separate from the older panel-only page versions/page rollback history.

crewship page project application <slug>

Backed by GET /api/v1/pages/{slug}/application. Prints the live publication receipt (publication, publication_version), the artifact, runtime_url and whether you may publish. A Page with no live application answers "publication": null. application takes no flags of its own.

crewship page project publications <slug>

Backed by GET /api/v1/pages/{slug}/project/publications. Lists immutable application publication receipts and the current publication counter, 50 per request. Use page project get <slug> --revision <source-revision> to inspect exact source before a reviewed rollback.

crewship page project unpublish <slug>

Backed by POST /api/v1/pages/{slug}/project/unpublish. Withdraws the custom application for all viewers. Requires Page ownership or workspace administration. Panels, source and retained publication history remain available. The version counter does not reset; stale requests return 409. Withdrawal does not cancel routines already queued, and repeating the same withdrawal is idempotent.

crewship page project action-status <slug> <pending-id>

Backed by GET /api/v1/pages/{slug}/application/actions/{pendingId}. page action --publication uses the published application action endpoint. The version must still be current and the Page declaration must match the publication. The existing routine RBAC/policy still applies. Reuse the same key for an explicit retry; a receipt means queued, not completed. action-status returns only the current user’s pending/run identifiers and states, never another reader’s run. A network timeout does not mean execution stopped. action-status takes no flags of its own.

crewship page project panel-history <slug> <panel>

Backed by GET /api/v1/pages/{slug}/application/panels/{panelId}/history, the route the @crewship/pages SDK’s getPanelHistory calls. Prints the panel’s readable payload ring — items of sequence, data, producedAt and state, newest first — plus next_before (0 at the end) and the publication it was read through. Nothing about the producer, its run or any other endpoint is on the wire; this is what a published application is allowed to see, and no more. The read is fenced on the publication: --publication is required, and a version that is no longer current, has been withdrawn, or whose recorded Page definition no longer matches the live one is answered with 409 (exit 5) — reload page project application for the current version. A panel you cannot see, or that does not exist, is 404 either way. Responses are bounded to 1 MiB; a single entry over that is 413.

crewship page project fsck <slug>

Backed by GET /api/v1/pages/{slug}/project/fsck. Checks referenced sources, checkpoint contents, artifacts and reachable Git objects, with Page edit permission. Returns healthy, object counts and failures; the command exits nonzero when unhealthy. It does not repair or publish anything. fsck takes no flags of its own.

crewship page project compact

Backed by POST /api/v1/pages/maintenance. Runs workspace-wide Page storage maintenance (OWNER or ADMIN required). Without flags, retains the configured recent history plus current draft/live/running roots. --discard-history --yes explicitly discards optional history across all Pages in this workspace while keeping those protected roots. This cannot recover space occupied entirely by protected current content.

Export and import with source

page export automatically uses crewship-page-bundle/v2 for a Page with source: one YAML contains the authored definition, sources, lockfile and small assets. Import accepts v1/v2 JSON or YAML, revalidates bindings and creates an inert draft: the imported page’s live definition carries no actions and nothing is published, while the draft’s definition keeps the call actions the export declared, ready to be reviewed and published here. It does not transfer grants, credentials, run data, Git history, build artifacts or trusted publication status. The referenced crews/routines must exist at the destination. Export is not a backup of the complete project history.

Agent chat: custom Page sources

Agents use the page_project tool on the existing crewship-routines MCP server. The sidecar supplies the workspace, crew and acting agent from its configuration and per-agent token. No human CLI/API token is needed in a crew. The operator must configure the protected projects directory, Git and the optional pinned build image described in the Pages Apps build profile.
  1. Create the operational Page using save_page; use its returned slug.
  2. Call page_project with operation: "init", slug and expected_revision: 0 to create the supported React/TypeScript starter.
  3. Use operation: "read" to inspect the current revision, file list, Page definition, the page’s path in the browser (page_path) and exact @crewship/pages SDK source. Add path to read one source file. The starter imports src/style.css; arbitrary design can be written in TSX/CSS within the supported dependency profile.
  4. Save a batch of whole-file replacements with the revision just read:
delete_paths removes existing files. definition optionally replaces the complete portable Page definition in the draft. The complete MCP request must fit in 1 MiB; use small batches, each against the latest returned revision. A conflict returns an error: read, reconcile and save again. Never initialize an existing project to reset it. Source writes create Git checkpoints; the published Page is unchanged.
  1. Call build with expected_revision, then poll status for running, ready, failed or interrupted. One build runs per instance; a busy worker refuses another request. Status returns diagnostics and build identity without retransmitting compiled JavaScript.
  2. Call check with the returned build_id and source expected_revision. Report the revision/build and page_path to the user: the review, with its preview, is in the page editor’s Content section. A successful static check does not replace browser or source security review.
Source initialization, saving and builds obey page_create autonomy policy; held or denied operations do not mutate a draft or start a build. Reads and checks require the Page’s exact owning crew and a current agent in that crew. The onboarding setup crew must explicitly name the target crew, following save_page delegation rules. Revisions/builds retain the actual author and the target owner separately. Run attribution is not inferred from an unverified ID. There is no publish operation in this agent tool. An authorized user publishes the reviewed build through the page editor (Edit → Content, or History for a retained version) or crewship page project publish. After a transport timeout the outcome can be unknown; inspect the current revision and build status before retrying. Data producers keep pushing through the existing panel API/page.write path independently of source edits.