diff --git a/content/(dynamic)/crons.mdx b/content/(dynamic)/crons.mdx index f060223b..d9e53a0d 100644 --- a/content/(dynamic)/crons.mdx +++ b/content/(dynamic)/crons.mdx @@ -9,7 +9,7 @@ There's no dashboard editor for crons. Declare schedules in `sf.jsonc`, then pub ## How a run happens -A cron is a scheduled `GET` against a path on your own site. It goes through the same front door a visitor gets, so whatever serves that path answers it: a [Zero](/zero-runtime) endpoint, a [Functions](/functions) route, or a static file. +A cron is a scheduled `GET` against a path on your own site. It goes through the same front door a visitor gets, so whatever serves that path answers it: a [Zero](/zero) page, a [Functions](/functions) route, or a static file. A response under 400 is a success and is silent. A response of 400 or worse, or a fatal error, is a failure and is reported. diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index f49b7949..8e3e3ac8 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -1,69 +1,90 @@ --- title: Database -description: Every Zero Space gets its own MySQL. Declare tables in your capsule, migrate on publish, read rows with sf db, and open a single-use SQL console when you need one. +description: Every Space gets its own MySQL. A Zero app's collections and tables live there, Spacefast migrates it on publish, and a single-use SQL console opens it when you need raw SQL. sidebar: order: 2 --- -There's no connection string for this database — your code reaches it through `ctx.db` or `env.DB`, or through a single-use SQL console when you need raw SQL. +There's no connection string for this database. Your code reaches it through Zero's `db` or a worker's `env.DB`, and you reach it through a single-use SQL console. :::note[Don't have a database yet?] -For [Zero](/zero-runtime), start with `sf init --runtime zero`, or add the full [runtime block](/zero-runtime#declare-it) to `sf.jsonc`, including `kind: "zero"`, `server`, and `client` entry paths. The server file is required; Zero can generate a client shell. Publish the app before querying its live database. A [Functions](/functions#what-is-on-env) worker also gets a database through `env.DB`, but the Zero table commands do not apply to it. +Start a [Zero](/zero) app with `sf init --runtime zero`. Its `zero.config.ts` declares the collections and tables, and Spacefast creates them when you publish. A [Functions](/functions#what-is-on-env) worker also gets the database, through `env.DB`. ::: ## What it is -Every Space that runs [Zero](/zero-runtime) gets one database, the MySQL that lives on the machine serving the site. A [Functions](/functions) worker gets one too, as `env.DB`. +Every Space has one database, the MySQL that lives on the machine serving the site. A [Zero](/zero) app keeps its collections and tables there. A [Functions](/functions) worker gets it as `env.DB`. -You do not create it, size it, or connect to it. Your capsule declares the tables. The platform applies the migration when a version finalizes. +You do not create it, size it, or connect to it. Your config declares the schema, and Spacefast applies it when a version initializes. :::warning[There is no external connection string] -No route, flag, or dashboard field returns a DSN, and none is coming. Your code reaches the database through `ctx.db` (Zero) or `env.DB` (Functions). For everything else there is the SQL console below. +No route, flag, or dashboard field returns a DSN. Your code reaches the database through `db` (Zero) or `env.DB` (Functions). For everything else there is the [SQL console](#the-sql-console). ::: ## Query it from code -In a Zero capsule the schema you declared is the API. +In a Zero app, hooks and actions read and write through `db`. Your schema is the API, and its column and relation names are checked at build time. -```ts server/index.ts -import { capsule, mutation, query, string, table } from "@spacefast/zero/server"; +```ts zero.config.ts +import { action, db, defineConfig, HttpError, integer, table, text } from "zero"; -export default capsule({ - name: "notes", - schema: { - notes: table({ body: string(), ownerId: string() }).index("by_owner", ["ownerId"]), +// Each signed-in account reads and writes only its own rows, through every route. +const notes = table( + { + body: text().notNull(), + ownerId: integer().notNull(), }, - queries: { - recent: query(async (ctx) => - ctx.db.notes - .withIndex("by_owner", (range) => range.eq("ownerId", ctx.auth.userId)) - .order("desc") - .take(20), - ), + { + access: { + read: ({ user }) => (user ? { ownerId: user.id } : false), + create: false, + update: false, + delete: false, + }, }, - mutations: { - add: mutation(async (ctx, body: string) => { - await ctx.db.notes.insert({ body, ownerId: ctx.auth.userId }); - ctx.invalidate("recent"); +); + +export default defineConfig({ + tables: { notes }, + actions: { + recent: action({ + input: {}, + access: "authenticated", + async handler(_input, ctx) { + if (!ctx.user) throw new HttpError(401, "unauthenticated", "Sign in first"); + return await db.query.notes.findMany({ + where: { ownerId: ctx.user.id }, + orderBy: { id: "desc" }, + limit: 20, + }); + }, + }), + add: action({ + input: { body: text().notNull() }, + access: "authenticated", + async handler({ body }, ctx) { + if (!ctx.user) throw new HttpError(401, "unauthenticated", "Sign in first"); + const [row] = await db.insert(notes).values({ body, ownerId: ctx.user.id }).returning(); + return row; + }, }), }, }); ``` -| Call | Available in | What it does | -| --- | --- | --- | -| `get(id)` | queries and mutations | One row by id, or `null` | -| `withIndex(index, range?)` | queries and mutations | A query builder over a declared index | -| `.order("asc" \| "desc")` | builder | Direction | -| `.collect()` | builder | Every matching row | -| `.take(n)` | builder | The first `n` | -| `.first()` | builder | One row or `null` | -| `.paginate({ cursor, numItems })` | builder | `{ page, continueCursor, isDone }` | -| `insert(value)` | mutations and write endpoints | Returns the new row | -| `update(id, patch)` | mutations and write endpoints | Returns the row or `null` | -| `delete(id)` | mutations and write endpoints | Returns a boolean | - -Index ranges take `eq`, `gt`, `gte`, `lt`, and `lte`. Every call is async, so await all of them. +The owner comes from `ctx.user`, the signed-in caller, never from `input`. Anything in `input` is whatever the client sent, so an `ownerId` there would let any signed-in account read or write another user's notes. The table's `access` rule enforces the same thing on the generated REST routes, which skip your actions. Without it a table is readable by anyone. Writes go only through `add`, so the table refuses direct writes. + +| Call | What it does | +| --- | --- | +| `db.query..findMany({ where, orderBy, limit, offset, columns, with })` | Matching rows | +| `db.query..findFirst({ where, ... })` | One row, or `null` | +| `db.insert(table).values(row \| rows).returning()` | Inserts, and returns the new rows | +| `db.update(table).set(patch).where(filter).returning()` | Updates, and returns the changed rows | +| `db.delete(table).where(filter).returning()` | Deletes, and returns the removed rows | + +`where` is an object filter: `{ col: value }`, or `{ col: { eq, ne, gt, gte, lt, lte, in, notIn, like, isNull, isNotNull } }`, with top-level `OR`, `AND`, and `NOT`. A relation key filters through the relation: `{ author: { name: "Alice" } }`. `orderBy` is `{ col: "asc" | "desc" }`. Await every call. + +An action's writes run in one transaction. A throw anywhere rolls all of them back. A Functions worker gets a D1-shaped binding instead. @@ -76,124 +97,54 @@ export default { }; ``` -## Inspect the schema - -```bash -sf db -``` - -`sf db` reads the live version's declared tables, its migration plan, and whether that plan has been applied. It takes an optional target, so `sf db docs` reads the space `docs` without `--space`. A space id, slug, live URL, or custom domain all work. - -```text -Backend mysql -Schema hash sha256:1f9c… -Tables notes, sessions -Migrations 0 pending -``` - -`plan.applied` can come back `null`. That means the control plane has no application record and refuses to guess, not that something failed. - -To read a locally running capsule instead of the live version, point at `sf dev`. - -```bash -sf db --port 4173 -sf db --local-url http://127.0.0.1:4173 -``` - -`--local-url` accepts `localhost`, `127.0.0.1`, or `::1` only. A refused port exits `40`. - -## Read rows - -```bash -sf db dump -sf db dump --table notes --limit 100 -``` +## Migrations -`--limit` is 1 to 100 and defaults to 25. `--table` must be a declared table name matching `/^[A-Za-z_][A-Za-z0-9_]*$/`. Omit it to dump every declared table. The dump reads through the space's live runtime, so it needs the app to be reachable. +Your `zero.config.ts` is the schema. When you publish a version that changes it, Spacefast diffs it against the schema the Space already runs and applies the difference while the new version initializes, before it can go live. -## Back up everything +To see the SQL a change produces, build locally and read the newest file in `zero/migrations/`: ```bash -sf db export -sf db export docs --out ./docs-backup.json +sf zero build ``` -`sf db export` walks every declared table in stable keyset pages of 500 rows and writes one JSON file. Without `--out` the file is `spacefast-backup--.json`. - -The write is atomic and mode `0600`. If the export fails part-way, an existing destination is left exactly as it was. - -## Migrations +:::warning[Rollback does not roll back the schema] +Every version of a Space shares one database, migrated forward to the newest schema. Rolling back to older code keeps the newer tables and columns, so make schema changes the older code can live with. +::: -Your capsule's tables are the schema. Publishing a version that changes them plans a migration, and the platform applies it at finalize. `sf db migrate` applies the live plan again, or previews what your source tree would change on top of it. +To rename a collection or table without moving its data, keep the old physical name with `storageName`: -```bash -sf db migrate -sf db migrate --drop -sf db migrate --rename +```ts +const stockItems = table(fields, { storageName: "stock_items" }); ``` - - - Edit `table(...)` and `.index(...)` in your server entry. - - - Run `sf db migrate` from the project directory. It computes the plan locally, prints the operations, and asks before applying. - - - Drops and renames are refused unless you pass `--drop` or `--rename`. The two are mutually exclusive. - - - -| Operation | Needs a flag | -| --- | --- | -| `create_table`, `add_column`, `add_index` | no | -| `drop_table`, `drop_column`, `drop_index` | `--drop` | -| `rename_table`, `rename_column` | `--rename` | - -`sf db migrate` decides Zero-ness from your local `sf.jsonc` before it contacts the API. A static or Functions project fails with `runtime_kind_mismatch` and exit code `30`, having sent no request. `--previous-artifact` diffs against a named artifact instead of the live schema. - -`sf db`, `sf db dump`, and `sf db migrate` are Zero-only. A worker declares no table set, so they answer `runtime_kind_mismatch` for one. +A build that drops one collection or table while adding another is refused as an ambiguous replacement. Rename with `storageName` instead, and remove storage in its own change. ## The SQL console -When you need raw SQL, the dashboard mints a single-use phpMyAdmin sign-in URL. - -Open **Database** for the space and click **Open db console**. The page shows **Backend**, **Migrations**, a **Database tables** list, and per-table **Columns** and **Indexes**. - -From the CLI: +When you need raw SQL, the CLI mints a single-use phpMyAdmin sign-in URL. ```bash sf db console sf db console --show-secret ``` -`sf db console` opens a browser by default, because the URL grants full SQL authority over the space's database. `--show-secret` prints it instead, and is the deliberate override. The URL is masked in `--json` receipts unless you pass it. - -The console is also how you reach a Functions worker's tables, since the schema routes do not apply to one. +`sf db console` opens a browser by default, because the URL grants full SQL authority over the Space's database. `--show-secret` prints it instead, and is the deliberate override. The URL is masked in `--json` receipts unless you pass it. Opening the console needs write access to the Space. -Until the space runs a Zero app or a worker, the Database page shows **Database isn't enabled**. +A Zero app's tables share a prefix that starts with `sfz_`. ## Limits | Limit | Value | | --- | --- | | Databases per Space | 1 | -| `sf db dump --limit` | 1 to 100, default 25 | -| Export page size | 500 rows, fixed | -| Field types you can declare | `string()`, `boolean()`, `id("table")` | -| Reserved column names | `id`, `createdAt`, `updatedAt` | -| Reserved index name | `by_creation` | +| Console URL | Single-use | ## API | Operation | Route | | --- | --- | -| [`getSpaceDatabase`](/api/reference/database/getspacedatabase) | `GET /v1/spaces/{spaceId}/db` | -| [`getSpaceDatabaseDump`](/api/reference/database/getspacedatabasedump) | `GET /v1/spaces/{spaceId}/db/dump` | -| [`getSpaceDatabaseExport`](/api/reference/database/getspacedatabaseexport) | `GET /v1/spaces/{spaceId}/db/export` | -| [`migrateSpaceDatabase`](/api/reference/database/migratespacedatabase) | `POST /v1/spaces/{spaceId}/db/migrate` | | `openSpaceDatabaseConsole` | `POST /v1/spaces/{spaceId}/db/console` | ## Next steps -Use [CLI database commands](/cli/db) to inspect tables or export data. +See [CLI database commands](/cli/db) for `sf db console` and crons. diff --git a/content/(dynamic)/environment-variables.mdx b/content/(dynamic)/environment-variables.mdx index 35b238a0..6f813e31 100644 --- a/content/(dynamic)/environment-variables.mdx +++ b/content/(dynamic)/environment-variables.mdx @@ -1,6 +1,6 @@ --- title: Environment variables -description: Set variables on a Space or a team, keep secrets write-only, substitute them into files at build, and read them from a handler as ctx.env +description: Set variables on a Space or a team, keep secrets write-only, substitute them into files at build, and read them from server code at runtime sidebar: order: 3 --- @@ -81,7 +81,7 @@ sf env set API_URL https://api.example.com --branch-value beta=https://beta.exam `--branch-value` takes `branch=value` and repeats. An empty branch or an empty value is a validation error. -### Runtime, `ctx.env` +### Runtime, `env()` Server-only values belong in `.env.server` next to your project. `sf publish` reads that file, syncs every key into the Space's variables **as secrets**, and never uploads the file itself. The shared publish denylist covers `.env.*`, so the bytes stay on your machine. @@ -92,24 +92,26 @@ EOF sf publish ``` -A Zero handler reads it from `ctx.env`. +A [Zero](/zero) action or hook reads it with `env()`, which returns `null` for a name that isn't set. -```ts server/index.ts -import { capsule, endpoint, json } from "@spacefast/zero/server"; +```ts zero.config.ts +import { action, defineConfig, env } from "zero"; -export default capsule({ - name: "billing", - endpoints: { - charge: endpoint({ mode: "write", method: "POST", path: "/api/charge" }, (ctx) => - json({ configured: Boolean(ctx.env.STRIPE_SECRET_KEY) }), - ), +export default defineConfig({ + actions: { + billingStatus: action({ + access: "editor", + handler() { + return { configured: env("STRIPE_SECRET_KEY") !== null }; + }, + }), }, }); ``` -A [Functions](/functions) worker reads the same values from `env`, its second handler argument. +A Zero app sees **Space** variables only. Team variables do not reach it, so set what the app needs on the Space. -`.env.lakebed.server` is accepted as an alias for capsule portability. +A [Functions](/functions) worker reads the same values from `env`, its second handler argument. An anonymous first publish, before you have an account, syncs nothing and reports `skipped: "anonymous"`. diff --git a/content/(dynamic)/functions.mdx b/content/(dynamic)/functions.mdx index 8f96dcb0..c316287d 100644 --- a/content/(dynamic)/functions.mdx +++ b/content/(dynamic)/functions.mdx @@ -5,9 +5,9 @@ sidebar: order: 1 --- -Functions can detect a supported worker layout when you publish, without an explicit `runtime` block. Custom layouts need a declaration, and [Zero](/zero-runtime#declare-it) is always declared explicitly. +Functions can detect a supported worker layout when you publish, without an explicit `runtime` block. Custom layouts need a declaration. A [Zero](/zero) app is a project with a `zero.config.ts`. -Functions runs your code as a worker. Use it when you need npm packages or framework output like OpenNext Next.js. If you want live queries in the browser and a schema the platform migrates for you, use [Zero](/zero-runtime) instead. Both can call `fetch()`. One version declares one runtime. +Functions runs your code as a worker. Use it when you need npm packages or framework output like OpenNext Next.js. If you want collections, tables, and actions compiled to WordPress, with a schema Spacefast migrates for you, use [Zero](/zero) instead. Both can call `fetch()`. One version declares one runtime. Functions is on for every account. diff --git a/content/(dynamic)/logs.mdx b/content/(dynamic)/logs.mdx index b10482b9..8b85884d 100644 --- a/content/(dynamic)/logs.mdx +++ b/content/(dynamic)/logs.mdx @@ -36,7 +36,7 @@ The first positional is a target, which can be a Space id, slug, live URL, or cu | `--follow`, `-f` | off | Polls every 2 seconds until Ctrl-C | | `--cursor` | none | Continues from the cursor the previous page printed | | `--request-id` | none | Runtime only. Everything logged while serving one request | -| `--handler` | none | Runtime only. Lines from one Zero mutation or Functions handler | +| `--handler` | none | Runtime only. Lines from one Functions handler | `--request-id` and `--handler` on the access log are refused rather than ignored, with `Flags --request-id and --handler only apply to \`sf logs runtime\`.` @@ -59,12 +59,12 @@ Runtime entries carry more. | `timestamp` | When the line was written | | `level` | `debug`, `info`, `warn`, or `error` | | `requestId` | Nullable. Pass it to `--request-id` | -| `handlerName` | Nullable. Zero names the mutation, Functions names the handler | +| `handlerName` | Nullable. The Functions handler that wrote the line | | `versionId` | Nullable | | `message` | What you logged | | `metadata` | The data bag you passed | -In a Zero capsule, `ctx.log.info(message, data)` writes here, as do `warn` and `error`. A static Space returns an empty runtime stream, because it runs no code. +A [Functions](/functions) worker's console output lands here. A static Space returns an empty runtime stream, because it runs no code. ## Two things that surprise people diff --git a/content/(dynamic)/meta.ts b/content/(dynamic)/meta.ts index d0c68f8e..b4ca900d 100644 --- a/content/(dynamic)/meta.ts +++ b/content/(dynamic)/meta.ts @@ -4,5 +4,5 @@ export default defineMeta({ title: "Dynamic", collapsed: false, order: 5, - pages: ["zero-runtime", "functions", "database", "environment-variables", "crons", "object-storage", "logs", "wordpress"], + pages: ["zero", "functions", "database", "environment-variables", "crons", "object-storage", "logs", "wordpress", "zero-migrate"], }); diff --git a/content/(dynamic)/object-storage.mdx b/content/(dynamic)/object-storage.mdx index 58681a11..f691da05 100644 --- a/content/(dynamic)/object-storage.mdx +++ b/content/(dynamic)/object-storage.mdx @@ -11,7 +11,7 @@ Storage holds files uploaded by a running app, separate from published version f Storage holds files a running app uploaded, not the files you published. Only a Space with a runtime app has it. Version files are a different thing and live in [Publishing](/publish). -An object is addressed by a bare 32-character hexadecimal id. There is no key, no path, and no visibility flag. The id is the identity. +An object is addressed by a bare 32-character hexadecimal id. There is no key and no path. The id is the identity. | Field | Notes | | --- | --- | @@ -28,28 +28,9 @@ An object is addressed by a bare 32-character hexadecimal id. There is no key, n `filename` is presentation only. Path separators, control characters, `.`, and `..` are refused rather than stripped, so the value you send is the value that comes back. -## Upload from a capsule +## Uploads in a Zero app -The client SDK is the upload path for a [Zero](/zero-runtime) app. - -```tsx client/upload.tsx -import { storage } from "@spacefast/zero/client"; - -async function onFile(file: File) { - const object = await storage.upload(file); - console.log(object.id, object.url); -} -``` - -`storage` has three methods. - -| Method | Returns | -| --- | --- | -| `upload(file: Blob)` | `{ id, contentType, size, url }` | -| `get(id: string)` | A `Response`, or `null` | -| `delete(id: string)` | Nothing | - -In local `sf dev`, uploads live in memory. +A [Zero](/zero) app handles its own uploads. Declare a `media()` column for images your content shows, or a `file()` column for private files that download through a signed link valid for five minutes. ## Upload from the API @@ -78,7 +59,7 @@ sf storage rm 0f3a2c19b74e4d0f8c6e5a2b1d9f4c73 `sf storage rm` confirms first. The id must be the 32-character id the list prints. A prefixed value like `private/` is refused. Delete is idempotent, so removing an object that is already gone prints ` was already gone.` rather than failing. -There is no `sf storage upload`. Uploads go through the client SDK, the API, or the dashboard. +Upload from the CLI with [`sf storage put `](/cli/storage#sf-storage-put), or through the API or the dashboard. ## Dashboard diff --git a/content/(dynamic)/wordpress.mdx b/content/(dynamic)/wordpress.mdx index 8457f5ee..ef4e5008 100644 --- a/content/(dynamic)/wordpress.mdx +++ b/content/(dynamic)/wordpress.mdx @@ -5,7 +5,7 @@ description: Every Space runs a WordPress behind its static files. Run WP-CLI ag Every Space includes a WordPress install behind its published files. Reach it through `sf wp` or its published Abilities. -Your published files serve in front of WordPress, so a static Space never touches it and never pays for it. It is there because it is what makes a Space more than a bucket: [Zero](/zero-runtime) apps run on it, [Storage](/object-storage) is its media library, and [Database](/database) is its MySQL. This page covers the two ways to reach that WordPress directly. +Your published files serve in front of WordPress, so a static Space never touches it and never pays for it. It is there because it is what makes a Space more than a bucket: [Zero](/zero) apps run on it, [Storage](/object-storage) is its media library, and [Database](/database) is its MySQL. This page covers the two ways to reach that WordPress directly. ## `sf wp` @@ -48,7 +48,7 @@ The host runs WP-CLI as a job and does not return its output; only success or fa Run with --local against a local WordPress to see what the command prints. ``` -When you need the bytes, run the same command locally, or reach for `sf db` and the database console. +When you need the bytes, run the same command locally, or open the database console with `sf db console`. ```bash sf wp --local --path ./wordpress core version @@ -76,12 +76,6 @@ The API route is `runSpaceWpCliCommand`. It takes an already-split argument vect An Ability is a named operation with a schema that the Space's WordPress publishes for agents to call. Some come from the runtime itself and exist on every Space. -```bash -sf zero abilities -``` - -The table prints `ABILITY | CATEGORY | MODE | REQUIRES`. The `REQUIRES` column is what answers "why did that come back 403". - ### Storage Abilities Five Abilities in the `zero-storage` category are registered by the runtime, so every Space publishes them whether or not it has ever published an app. They are the media library, the same objects [Storage](/object-storage) manages. @@ -100,19 +94,7 @@ WordPress decides which file types `zero/storage-upload` accepts. ### Call one -```bash -sf zero call zero/storage-list -sf zero call zero/storage-list --input '{"folder":"press","perPage":20}' -sf zero call zero/storage-upload --input @upload.json -``` - -`sf zero call` mints a short-lived token for the Space, runs the Ability against the Space's own WordPress REST API, and prints the HTTP status and the JSON result. The token is never printed and never appears in the `--json` receipt. - -`--input` takes inline JSON or `@path` to read it from a file. `--credential mch_...` picks a machine credential when the Space has more than one. A non-2xx answer is `zero_ability_failed` and exits 1, with the whole receipt in `details`. A 403 means the credential's Grant does not permit that Ability. - -### Mint a token yourself - -An agent working from the API does the same thing in two calls. +Mint a short-lived token, then call the Ability on the Space's own host. ```bash curl -X POST https://api.spacefast.com/v1/spaces/spc_xxxxxxxx/zero/token \ @@ -125,7 +107,7 @@ curl -X POST https://api.spacefast.com/v1/spaces/spc_xxxxxxxx/zero/token \ | Field | Value | | --- | --- | -| `credentialId` | A live `mch_` machine credential of this Space. Required | +| `credentialId` | A live `mch_` machine credential of this Space. Required. Create one with `sf share token create`. See [Machine credentials](/share#machine-credentials) | | `ttlSeconds` | 60 to 3,600. Defaults to 900 | Send the token to the Space in `X-SF-Authorization`, never in `Authorization`: @@ -142,11 +124,13 @@ The plain `Authorization` header belongs to your application and passes through The token is bound to the host the Space serves on, and the runtime re-derives what it may do from the credential's Grant on every request. The `capabilities` in the response are what to expect, not a promise: a credential scoped away from a path answers there with less, never with more. -`listSpaceZeroAbilities` is the API side of `sf zero abilities`. +### Your app's Abilities + +The live version publishes Abilities of its own for its content model. [`listSpaceZeroAbilities`](/api/reference/spaces/listspacezeroabilities) lists them, each with its `name`, its input and output schemas, and what it `requires`. Call one the same way, with its `name` in the path. The storage Abilities above are not in that list, because the runtime registers them on every Space. ## The database -The same WordPress owns a MySQL database. `sf db` reads its schema and rows, and the dashboard mints a single-use SQL console URL for raw queries. See [Database](/database). +The same WordPress owns a MySQL database. `sf db console` mints a single-use SQL console URL for raw queries. See [Database](/database). ## Next steps diff --git a/content/(dynamic)/zero-migrate.mdx b/content/(dynamic)/zero-migrate.mdx new file mode 100644 index 00000000..2a7e21aa --- /dev/null +++ b/content/(dynamic)/zero-migrate.mdx @@ -0,0 +1,134 @@ +--- +title: Migrate from Zero 0.8 +description: Publishing a Zero 0.8 app moves it to the current Zero whenever nothing would be lost. When something would, it publishes as 0.8 and tells you exactly what to change. +sidebar: + order: 8 +--- + +Your Zero 0.8 app keeps working. Publish it with a current CLI and it moves to the current Zero, the `zero.config.ts` compiler described in [Dynamic sites with Zero](/zero), whenever nothing would be lost. When something would, it publishes as 0.8, keeps serving, and the receipt lists each blocker. New Spaces start on the current Zero. + +A Zero 0.8 app is one built on `@spacefast/zero`: a `capsule()` in `server/index.ts`, declared in `sf.jsonc` as `"runtime": { "kind": "zero" }`. + +## Keep working on a 0.8 app + +A 0.8 app keeps its own local dev server and database commands. The [`sf zero`](/cli/zero) development commands are for `zero.config.ts` apps, not for this one. The exception is `sf zero migrate app`, which [previews the conversion](#preview-the-conversion). + +### Run it locally + +From the 0.8 project root, `sf dev` starts the 0.8 dev server, because `sf.jsonc` declares `"runtime": { "kind": "zero" }`. + +```bash +sf dev +sf dev --state-backend sqlite +sf dev --no-watch +``` + +| Flag | Default | What it does | +| --- | --- | --- | +| `--port=` | `4173` | Port for the dev server | +| `--state-backend=memory\|sqlite` | `memory` | Where local state lives. `memory` resets on every start | +| `--watch` / `--no-watch` | `--watch` | Poll source files and reload after changes | +| `--watch-interval=` | `1000` | Polling interval in milliseconds | +| `--dry-run` | off | Print the dev server plan without starting it | + +`sf dev` binds `127.0.0.1` unless you pass `--host` with `--allow-network`. + +### Read and migrate its database + +These commands read the tables the 0.8 app declares. A [Functions](/functions) worker declares none, so they answer it with `db_not_available` or `runtime_kind_mismatch`. `sf db console` works on any Space. See [Database](/database#the-sql-console). + +```bash +sf db +sf db list +sf db dump --table todos --limit 100 +sf db export --output ./backup.json +sf db migrate +``` + +| Command | What it does | +| --- | --- | +| `sf db [target]` | Tables, columns, and the pending migration plan | +| `sf db list [target]` | Tables and columns. Aliased as `sf db ls` | +| `sf db dump [target]` | Rows from every declared table. `--table` picks one, `--limit` caps rows per table (default 25, at most 100), and `--limit 0` prints row counts only | +| `sf db export [target]` | Every declared table to one versioned JSON backup. `--output` names the file. An existing file is replaced only after the whole export succeeds | +| `sf db migrate [source]` | Re-apply the live version's schema migrations, and preview what the source tree would change on top. `--drop` and `--rename` preview those operations without applying them | + +Pass `--port ` or `--local-url ` to `sf db`, `sf db list`, `sf db dump`, or `sf db export` to read a running `sf dev` server instead of the live version. + +## Migrate by publishing + +From the 0.8 project root, publish as usual: + +```bash +sf publish --space spc_123 --json +``` + +The CLI converts the app on your machine and checks the result. Then one of two things happens: + +| Result | What you see | +| --- | --- | +| Nothing would be lost | The Space moves to the current Zero, on its existing tables. `version.zero.migration.status` is `migrated` | +| Something would be lost | The app publishes as 0.8 and keeps serving. `status` is `refused`, and `diagnostics` lists each blocker with its `code`, `file`, `line`, `feature` and `fix` | + +Fix what `diagnostics` names and publish again. Nothing about your app changes until a publish migrates it. `waiting_on_runtime` means the app can move but the Space's runtime can't run the current Zero yet. It publishes as 0.8, and the next publish after the runtime update migrates it. + +## Preview the conversion + +To read the converted source before you publish, run the converter yourself. It writes a new project and never touches the Space. + +```bash +sf zero migrate app --from zero-0.8 ./old-app --out ./candidate --space spc_123 --dry-run +sf zero migrate app --from zero-0.8 ./old-app --out ./candidate --space spc_123 +sf zero check ./candidate +``` + +`./old-app` is the 0.8 project root, the directory with `sf.jsonc` and `server/index.ts`. `--out` must be a new directory, and a refusal writes nothing. Pass the Space's ID with `--space` so the converter binds each table to the exact MySQL table the Space already stores it in, or `--fresh` for an app that has no Space yet. Add `--users` when the Space has Space Users turned on but the capsule has no `auth` declaration. + +The command prints a JSON report: + +| Field | What it holds | +| --- | --- | +| `entries` | Every declaration, `mapped` or `review` | +| `errors` | Everything the converter refused, each with an RFC 6901 pointer into your source | +| `adoption_sql` | The SQL that checks the existing tables before first use | +| `connector_roles` | Connector role mappings, also written to `connector-roles.json` | + +The exit code is `0` for a candidate or a clean dry run, `1` for a refusal, and `2` for bad arguments. Fix every `errors` pointer, then read every `review` entry. A candidate with review entries still compiles, but it may not behave the way your 0.8 app did. + +## What converts + +| Zero 0.8 | Current Zero | +| --- | --- | +| `capsule()` in `server/index.ts` | `defineConfig()` in `zero.config.ts` | +| `schema` tables | `table()` entries under `tables`, bound to the existing MySQL tables | +| `query()` | An action that reads | +| `mutation()` | An action that writes, in one transaction | +| `action()` | An action | +| `ctx.db` `get`, `insert`, `update`, `delete`, and static `withIndex` chains ending in `collect`, `count`, `first`, or `take` | `db.query` and `db.insert`/`update`/`delete` | +| `auth` | A `members` user projection with a unique email. `auth.requireSignIn` becomes a signed-in rule on each action | +| Native `posts`, or a `content/posts` directory | A `posts` collection with title and rich-text body | +| Connectors | `connector-roles.json`: `owner` becomes `personal`, `team` and `visitor` stay | + +Ported tables keep their storage. IDs stay text in action results, numbers stay doubles, and booleans match both the old `true`/`false` text and the new `1`/`0`. The converter copies no rows and renames no columns. + +Every write starts denied. Open `zero.config.ts` and set `access` on each collection, table, and action before you rely on it. + +## What you port by hand + +The converter refuses these, with a pointer to each: + +- **Endpoints.** Rewrite each one as a named action at `POST /wp-json/zero/v1/actions/`, then update its callers. +- **Pages, layouts, and islands.** Rebuild them as [TSX pages](/zero#pages). +- **Connector calls inside handlers.** +- **Realtime.** Declarations are dropped with a review hint. The current Zero has no live queries, so a client reads again after a write. +- **Pagination, dynamic query builders, and range comparisons on optional fields.** +- **npm imports and unknown `ctx` services.** +- **Row IDs above JavaScript's safe integer range.** + +The converter reads only the server. Your `client/` code uses `@spacefast/zero/client`, which has no counterpart: call the generated client in `zero/client/` from your frontend instead, or move the UI to TSX pages. + +## Publishing a hand-ported app + +Publish the 0.8 project, not a hand-edited candidate. A Space's data moves with the app: publishing a `zero.config.ts` app to a Space that still runs Zero 0.8 is refused with `zero_migration_required`, because it would leave the posts, users, and sessions behind. Once a publish has migrated the Space, edit and publish the current Zero app as usual. + +A project with both `"runtime": { "kind": "zero" }` in `sf.jsonc` and a `zero.config.ts` fails to publish with `zero_mixed_generation`. Delete that block if it has one. diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero-runtime.mdx deleted file mode 100644 index f3998ae0..00000000 --- a/content/(dynamic)/zero-runtime.mdx +++ /dev/null @@ -1,287 +0,0 @@ ---- -title: Dynamic sites with Zero -description: Zero is the runtime a Space gets when its project declares it. A database, HTTP endpoints, live queries, crons, and storage, from one TypeScript app. -sidebar: - order: 0 ---- - -Zero is available on every account, with [usage allowances and metered rates](/usage). To use it, declare the full [runtime block](#declare-it) in `sf.jsonc` and provide the server entry. - -## What Zero is - -Zero is the runtime a Space gets when its project declares it. Your server code runs on the machine that already serves the site, next to that space's own MySQL database, inside a QuickJS runner. You write one TypeScript app. It declares tables, queries, mutations, actions, and HTTP endpoints. `sf publish` compiles it into a **capsule** the platform installs on the space. - -There is no account-level switch to enable. - -The other runtime is [Functions](/functions), a worker for npm-heavy code and framework output like OpenNext. One version declares one runtime, not both. - -## Declare it - -Zero is never inferred. A runtime that owns a database does not spring into existence from a directory shape, so both entry paths are required. - -```jsonc sf.jsonc -{ - "$schema": "https://spacefast.com/schemas/sf.json", - "name": "my-app", - "runtime": { - "kind": "zero", - "server": "server/index.ts", - "client": "client/index.tsx" - } -} -``` - -The fastest way to get that file plus a working app is the scaffold. - -```bash -sf init my-app --runtime zero --template todo -cd my-app -sf dev -sf publish -``` - -`--template` accepts `todo`, `guestbook`, or `contact`. The client entry is optional on disk. If no file backs it, the build generates a shell and the capsule is server-only. The server entry is not optional. - -## What you get - -| Piece | What it is | Page | -| --- | --- | --- | -| Database | The space's own MySQL. Your capsule declares the tables; the platform migrates them when a version finalizes. | [Database](/database) | -| Endpoints | Plain HTTP routes in your capsule, for webhooks and callers outside the app. | This page | -| Queries, mutations, and actions | The read, transactional write, and outbound-work paths your client calls. Queries are live. | This page | -| Environment variables | `ctx.env`, from `.env.server` or `sf env`. | [Environment variables](/environment-variables) | -| Crons | Scheduled `GET` requests to your own paths, declared in `sf.jsonc`. | [Crons](/crons) | -| Storage | Runtime object storage for visitor uploads. | [Storage](/object-storage) | -| Logs | What your handlers wrote, plus every request the edge served. | [Logs](/logs) | - -## The `@spacefast/zero` package - -The scaffold's `package.json` pins the package to the compiler's own version. Nothing you install reaches the page. The build resolves the SDK, preact, and the rest from the platform, so these dependencies exist for types and editor completion. - -| Import | Use it from | What it holds | -| --- | --- | --- | -| `@spacefast/zero/server` | `server/` | `capsule`, `query`, `mutation`, `action`, `endpoint`, `table`, field constructors, response helpers | -| `@spacefast/zero/client` | `client/` | `useQuery`, `usePaginatedQuery`, `useMutation`, `useAction`, `useAuth`, `storage`, `Router` and friends | -| `@spacefast/zero/kit` | `client/` | The component set: `Button`, `Card`, `Input`, `Dialog`, `Table`, and about 30 more | -| `@spacefast/zero/charts` | `client/` | `LineChart`, `BarChart`, `Sparkline`, `StatTile` | - -`action`, `useAction`, and the `actions` capsule field require compiler 0.5.0 or later. - -## The minimal app - -Server code declares the schema and the handlers. - -```ts server/index.ts -import { boolean, capsule, endpoint, mutation, query, string, table, text } from "@spacefast/zero/server"; - -export default capsule({ - name: "notes", - schema: { - notes: table({ - body: string(), - done: boolean().default(false), - ownerId: string(), - }).index("by_owner", ["ownerId"]), - }, - queries: { - notes: query(async (ctx) => - ctx.db.notes - .withIndex("by_owner", (range) => range.eq("ownerId", ctx.auth.userId)) - .order("desc") - .collect(), - ), - }, - mutations: { - addNote: mutation(async (ctx, body: string) => { - await ctx.db.notes.insert({ body, done: false, ownerId: ctx.auth.userId }); - ctx.invalidate("notes"); - }), - }, - endpoints: { - status: endpoint({ mode: "read", method: "GET", path: "/api/status" }, () => text("ok")), - }, -}); -``` - -Client code reads through queries and writes through mutations. - -```tsx client/index.tsx -import { useMutation, useQuery } from "@spacefast/zero/client"; -import { Button, Card } from "@spacefast/zero/kit"; - -export function App() { - const notes = useQuery<{ id: string; body: string }[]>("notes"); - const addNote = useMutation<[body: string], void>("addNote"); - return ( - - -
    {notes.map((note) =>
  • {note.body}
  • )}
-
- ); -} -``` - -`capsule()` takes `name`, `favicon`, `schema`, `queries`, `mutations`, `actions`, and `endpoints`, and throws on a key it does not know. - -### Tables - -`table(fields)` takes five field constructors: `string()`, `boolean()`, `number()`, `id("otherTable")`, and `userId()`. Store dates as strings. The wire schema also accepts `json`, but the SDK has no `json()` field constructor. `.default(value)` makes a field optional on insert. Every row gets `id`, `createdAt`, and `updatedAt`, so those three names are reserved, as is the built-in `by_creation` index. Field and index names must match `/^[A-Za-z_][A-Za-z0-9_]*$/`. A leading underscore is allowed. Declare an index with `.index(name, fields)` and read it through `withIndex`. - -### Handler context - -Every query, mutation, action, and endpoint handler takes `ctx` first. - -| On `ctx` | Queries and read endpoints | Mutations and write endpoints | Actions | What it is | -| --- | --- | --- | --- | --- | -| `db` | read only | read and write | read only | `get`, `withIndex`, and on the write side `insert`, `update`, `delete` | -| `auth` | yes | yes | yes | `userId`, `displayName`, `isGuest`, `isSignedIn`, `isAuthenticated`, `provider` | -| `env` | yes | yes | yes | Server-only variables. See [Environment variables](/environment-variables) | -| `log` | yes | yes | yes | `info`, `warn`, `error`, each with an optional data bag | -| `invalidate` | no | yes | no | Names the live queries this mutation changed | - -A signed-out visitor is a guest with a real `userId` (`guest:…`), so `!ctx.auth.userId` never rejects anyone. Gate sign-in on `ctx.auth.isAuthenticated`. - -A query can run again when it satisfies a subscription, so any `fetch()` it makes can run again too. Queries have no `invalidate`; only write handlers tell subscriptions to refresh. - -An action runs without a transaction. It reads the database but cannot write to it, and it has no `invalidate`. Use it when a call causes an external effect, not to gain more runtime: the whole action has a 5-second deadline. To keep what it fetched, hand the result to a mutation. The client calls one with `useAction(name)`. - -`invalidate()` takes **query names, not table names**. `ctx.invalidate("notes")` refreshes every `notes` subscription whatever arguments it carries. Calling it with nothing is the safe default and refreshes every live query on the page. - -### Endpoints - -`endpoint(route, handler)` takes `{ method, path, mode }`, where `mode` is `"read"` or `"write"`. Both `method` and `path` must be literal strings, because the compiler reads them from source without running it. The handler gets `(ctx, req)`, where `req` carries `method`, `path`, `url`, `headers`, `query`, and `text()` / `json()` / `bytes()`. - -Return one of the response helpers: `json(value)`, `text(value)`, `empty()` for a 204, `redirect(url)` for a 302, or `new ImageResponse(element)` for a rendered 1200x630 image. - -## The request lifecycle - -1. Static bytes win. Anything committed in the version serves from disk without waking your code. -2. Platform paths are reserved. First segments `__spacefast`, `__stattic`, and `__span` belong to the platform, as do `/auth/` and `/_spacefast/` inside the app. -3. Your endpoint table matches. A hit runs the handler in the QuickJS runner on the space's own machine, in one transaction with the database. -4. Everything else falls to the app shell, which serves your client bundle. - -Queries, mutations, and actions do not get their own URLs. The client sends them over `/__zero/run`. When a mutation commits, the runtime publishes an invalidation event. Every browser holding a matching subscription re-runs its query. That is the whole realtime story. Do not poll. - -An endpoint cannot claim `/`, `/index.html`, `/client.js`, `/auth/callback`, anything under `/auth/`, or anything under `/_spacefast/`. The shell, the client bundle, and the sign-in flow already answer those. - -## What a capsule is, and what compiles - -A capsule is the value `capsule({...})` returns. `sf publish` runs esbuild over your tree. It produces one ESM server bundle for the runner, one browser client bundle, and a migration plan for the tables you declared. Rolling a version back rolls all three back together, because they travel with the version. - -Only three source roots are read: `client/`, `server/`, and `shared/`. Beyond them the compile admits `package.json`, `.env.server`, and `.env.lakebed.server` and nothing else. `shared/` imports its own relative files only, no packages at all. Client code cannot import the server SDK, and server code cannot import client files. - -| Limit | Value | -| --- | --- | -| Server bundle | 768 KiB | -| Client bundle, your code and your npm packages | 8 MiB | -| Server imports | `@spacefast/zero/server`, `preact`, `preact/jsx-runtime`, `preact/jsx-dev-runtime` | -| Client imports | Anything except the server SDK and `node:*` built-ins | -| Endpoint request body | 2 MiB | -| Rejected everywhere | `eval`, `Function`, `require()`, dynamic `import()`, `process`, `__dirname`, `__filename`, `Bun`, `Deno` | -| Rejected in `server/` and `shared/` | `window`, `document`, `localStorage`, `WebSocket`, `Worker`, `XMLHttpRequest`, `globalThis` | - -Platform modules do not count against your client cap. The SDK, kit, charts, preact, recharts, and lucide serve from immutable same-origin URLs the browser keeps across publishes. - -### Outbound HTTP - -`fetch()` works in every Zero handler, HTTPS only. A mutation or write endpoint holds its database transaction open while it waits, so each fetch has a 5-second timeout. Queries and read endpoints get 10 seconds per fetch. An action gets 5 seconds for the whole invocation, shared across every fetch it makes. - -How far `fetch()` reaches depends on who owns the space: - -| Space | Reaches | -| --- | --- | -| Claimed | Any public HTTPS host | -| Unclaimed | Only the trusted list: `api.anthropic.com`, `api.openai.com`, `api.github.com`, `api.stripe.com`, `api.groq.com`, `api.mistral.ai`, `api.together.xyz`, `generativelanguage.googleapis.com`, `openrouter.ai` | - -From an unclaimed space, `fetch()` to any other host throws a `SpacefastFetchError` with code `zero_fetch_host_untrusted` and the message "This host isn't reachable from an unclaimed space. Claim the space to reach any public host." Claiming opens it up without a republish. Private addresses and cloud metadata stay blocked either way. - -Outbound access has nothing to do with who can open your space. Calling an API does not make the space public or touch its Grants. - -### Example: invite People from your app - -There is no `ctx.people`. To invite someone to a private space, or resend their invite, call the People API from an action with a [machine credential](/share#machine-credentials). - -```bash -sf share token create --name invites --path '/**' --role manager --show-secret --json \ - | jq -r '.data.token' \ - | sf env set INVITE_TOKEN --value-from-stdin -sf env set INVITE_SPACE_ID spc_123 -sf env set INVITE_ADMINS user-id-1,user-id-2 -``` - -The `manager` role carries `access.manage`, which the People routes require. Narrow `--path` to limit where the token can invite people. An invite with `scope: "/docs"` requires `--path '/docs/**'`; the bare `/docs` path does not cover that subtree. `INVITE_ADMINS` lists the `ctx.auth.userId` values allowed to send invites. The People API only works on a claimed space. Each `sf env set` queues a settings-only finalize; the action sees the new value after that operation finishes. - -An action is callable by anyone who can load your app, so gate it before it spends that token. - -```ts server/index.ts -import { action, capsule } from "@spacefast/zero/server"; - -type Ctx = { - auth: { isAuthenticated: boolean; userId: string | null }; - env: Record; -}; - -async function callPeople(ctx: Ctx, path: string, body?: unknown) { - const admins = (ctx.env.INVITE_ADMINS ?? "").split(",").map((id) => id.trim()); - if (!ctx.auth.isAuthenticated || !admins.includes(ctx.auth.userId ?? "")) { - throw new Error("You can't send invites."); - } - const response = await fetch( - `https://api.spacefast.com/v1/spaces/${ctx.env.INVITE_SPACE_ID}/people${path}`, - { - method: "POST", - headers: { - authorization: `Bearer ${ctx.env.INVITE_TOKEN}`, - "content-type": "application/json", - }, - body: body === undefined ? undefined : JSON.stringify(body), - }, - ); - if (!response.ok) throw new Error(`People API answered ${response.status}.`); - return (await response.json()).data; -} - -export default capsule({ - actions: { - invite: action((ctx, email: string) => - callPeople(ctx, "", { email, grants: [{ scope: "/", role: "viewer" }] }), - ), - resendInvite: action((ctx, personId: string) => - callPeople(ctx, `/${encodeURIComponent(personId)}/resend`), - ), - }, -}); -``` - -Call them from the client with `useAction("invite")`. `scope: "/"` covers the whole space; `/docs` covers one subtree. Both routes return the Person in `{ data }`. The invite link itself only ever travels by email. - -## Styling - -Write Tailwind classes in your JSX. Zero compiles them at publish, so do not add a CSS, PostCSS, or Tailwind pipeline. `@plugin` and `@config` directives are rejected, and `theme.json` plus utility classes are the whole styling story. - -Class names must be static strings. The compiler finds them by scanning source text, so `bg-${tone}-500` produces no CSS for that class. Branch to whole literals instead. - -Prefer the semantic tokens, which re-skin from `theme.json` and handle light and dark on their own: `canvas`, `surface`, `ink`, `ink-muted`, `line`, `accent`, `success`, `warning`, `danger`. The shadcn names (`bg-background`, `text-muted-foreground`, `bg-primary`) alias onto the same tokens. - -## Run it locally - -```bash -sf dev -``` - -`sf dev` starts the capsule dev server on `http://127.0.0.1:4173`. State lives in memory and resets on restart. Pass `--state-backend sqlite` to keep it between runs. `--watch` is on by default and reloads the runtime when a source file changes. A non-loopback `--host` needs `--allow-network`. - - - - Migrations, dumps, exports, and the SQL console. - - - Workers, the `functions/` file router, and OpenNext. - - - Every key the config schema accepts. - - - Follow what your handlers log. - - diff --git a/content/(dynamic)/zero.mdx b/content/(dynamic)/zero.mdx new file mode 100644 index 00000000..329f5438 --- /dev/null +++ b/content/(dynamic)/zero.mdx @@ -0,0 +1,272 @@ +--- +title: Dynamic sites with Zero +description: Zero compiles one zero.config.ts (collections, tables, actions, and TSX pages) into WordPress, and Spacefast hosts it on your Space. +sidebar: + order: 0 +--- + +Zero turns one `zero.config.ts` into a WordPress app: collections, tables, actions, access rules, and pages. `sf publish` sends the source, and Spacefast compiles it and runs it on the Space's own WordPress. Zero is available on every account, with [usage allowances and metered rates](/usage). + +## What Zero is + +Zero is a compiler. You describe your data and your server logic in TypeScript, and Zero compiles it to WordPress plugins: a post type for each collection, a SQL table for each table, REST routes for both, and plain PHP for your hooks and actions. Nothing runs JavaScript on the server. + +A project is a Zero app when it has a `zero.config.ts` at its root. There is no runtime block to declare and no switch to turn on. + +The other runtime is [Functions](/functions), a worker for npm-heavy code and framework output like OpenNext. + +## Start an app + +```bash +sf init my-app --runtime zero +cd my-app +sf zero dev +sf publish +``` + +`sf init --runtime zero` writes a starter `zero.config.ts` named after the directory. `sf zero init` writes the same file. `sf zero dev` runs the app on a local WordPress and rebuilds when you save. `sf publish` publishes it to a new Space. + +The starter holds one collection, one table, and one action: + +```ts zero.config.ts +import { action, collection, db, defineConfig, table, text, timestamp } from "zero"; + +// Collections are WordPress posts: they show up in wp-admin and the REST API. +export const posts = collection({ + title: text().notNull(), + body: text(), +}, { title: "title" }); + +// Tables are plain MySQL tables for app data. Signups hold visitors' emails, so only the +// signup action writes them and only admins read them. +export const signups = table({ + email: text().notNull().unique().max(191), + createdAt: timestamp().notNull().defaultNow(), +}, { access: { read: "admin", create: false, update: false, delete: false } }); + +export default defineConfig({ + name: "my-app", + collections: { posts }, + tables: { signups }, + actions: { + signup: action({ + input: { email: text().notNull() }, + access: "anyone", + async handler({ email }) { + const [row] = await db.insert(signups).values({ email: email.trim().toLowerCase() }).returning(); + return { id: row?.id ?? null }; + }, + }), + }, +}); +``` + +Everything comes from the `"zero"` module. + +## What you get + +| Piece | What it is | +| --- | --- | +| Collections | Editorial content. Each is a WordPress post type with the block editor, drafts, and revisions | +| Tables | App data in its own SQL table | +| REST routes | `/wp-json/zero/v1/` for every collection and table, with relations loaded through `with` | +| Actions | Named server functions at `POST /wp-json/zero/v1/actions/` | +| Hooks | Code that runs before or after a collection write, inside the same transaction | +| Access rules | Who may read and write each collection, table, field, and action | +| Sign-in | Accounts, sessions, guests, passkeys, and MFA through the `zero-identity` plugin | +| Jobs and webhooks | Background work that runs after a request commits | +| Pages | TSX routes and layouts that WordPress renders per request | +| Typed client | A `fetch` client generated from your schema | + +Your data lives in the Space's MySQL database. See [Database](/database). + +## Collections and tables + +Pick the storage by what the data is. A `collection()` is content people edit: it gets a WordPress post type, an editor in wp-admin, drafts, and revisions. A `table()` is app data: it gets its own SQL table and nothing else. + +```ts zero.config.ts +import { collection, defineConfig, defineRelations, integer, richText, table, text } from "zero"; + +const posts = collection({ title: text().notNull(), body: richText() }, { content: "body" }); +const stock = table({ + postId: integer().references(() => posts.id, { onDelete: "cascade" }), + quantity: integer().notNull().default(0), +}); + +const collections = { posts }; +const tables = { stock }; +const relations = defineRelations({ ...collections, ...tables }, (r) => ({ + stock: { post: r.one.posts({ from: r.stock.postId, to: r.posts.id }) }, + posts: { stock: r.many.stock() }, +})); + +export default defineConfig({ collections, tables, relations }); +``` + +Every row gets an `id`. The registry key is the name everywhere: `shoppingList` is `zero.shoppingList` in the client and `/wp-json/zero/v1/shopping_list` over REST. + +Column builders include `text()`, `integer()`, `real()`, `boolean()`, `timestamp()`, `date()`, `json()`, `email()`, `url()`, `slug()`, `richText()`, `media()`, `file()`, `select()`, `group()`, `repeater()`, and `blocks()`. Chain `.notNull()`, `.default(value)`, `.unique()`, `.min(n)`, `.max(n)`, `.pattern(/re/)`, or `.references(...)` to constrain one. A modifier that makes no sense for its column, like `.max()` on a boolean, fails the build at that line. + +## Access rules + +Every collection and table takes `access: { read, create, update, delete }`. Each rule is `true`, `false`, `"anyone"`, `"authenticated"`, a role, or a function of `ctx`. Roles rank `"guest" < "subscriber" < "author" < "editor" < "admin"`, and a role admits itself and everything above it. + +```ts +{ access: { read: "anyone", create: "author", update: "author", delete: "editor" } } +``` + +The default is read by `"anyone"`, write by `"editor"`. Actions default to `"editor"` too, so an action a visitor calls needs `access: "anyone"`. The rules hold on every path into the data: Zero's REST routes, WordPress's own `/wp/v2` routes, wp-admin, and Abilities. + +## Actions + +An action takes typed input and returns JSON. `input` uses the same column builders as your schema, so a bad request is refused before your handler runs. + +```bash +curl -X POST https://my-site.view.fast/wp-json/zero/v1/actions/signup \ + -H 'Content-Type: application/json' \ + --data '{"email":"sam@example.com"}' +``` + +A successful call answers `{ "data": ... }` with whatever the handler returned. Throw `new HttpError(status, code, message)` to choose the error response. Any throw rolls back every write the request made. + +## Hooks + +Hooks run around collection writes: `beforeCreate`, `afterCreate`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, and `afterDelete`. A `before` hook can change the data or throw to refuse the write. + +```ts +hooks: { + posts: { + beforeCreate(data) { + data.title = data.title.trim(); + if (data.title === "") throw new HttpError(422, "invalid_input", "A post needs a title"); + }, + }, +}, +``` + +## What compiles + +Hooks, actions, and access functions are a checked subset of TypeScript compiled to PHP. Functions, closures, spread, classes, `async`/`await` on Zero calls, and the usual control flow all work. `eval`, generators, `any`, and dynamic `import()` do not. Anything outside the subset fails the build at `file:line:col` with a fix: + +```bash +sf zero check +sf zero check --json +``` + +Imports can be `"zero"`, your own relative modules, and installed source packages. `sf publish` captures the installed packages it needs. Spacefast never runs `npm install`. + +## Call it from a frontend + +`sf zero build` and `sf zero dev` write a typed client to `zero/client/`. It is plain `fetch`, so it works from any framework. + +```ts +import { createClient } from "./zero/client/index.ts"; + +const zero = createClient({ url: "https://my-site.view.fast/wp-json/zero/v1" }); + +const posts = await zero.posts.list({ orderBy: { title: "asc" } }); +await zero.actions.signup({ email: "sam@example.com" }); +``` + +Every argument and result is typed from your schema. A published Space also serves its client at `/__spacefast/client.js`, with types at `/__spacefast/client.d.ts`. + +To serve a static frontend from the same Space, put its files in `public/`. `sf publish` uploads them with the app. + +## Pages + +Pages are TSX files under `pages/`. They compile at build time, and WordPress renders them on every request with the visitor's own data, through the same access rules as the REST routes. + +| File | Route | +| --- | --- | +| `pages/page.tsx` | `/` | +| `pages/about/page.tsx` | `/about` | +| `pages/notes/[slug]/page.tsx` | `/notes/`, one per post in a collection, with `collectionPage("notes")` | +| `pages/layout.tsx` | Wraps every page below it | +| `pages/not-found.tsx` | The body of a 404 | + +A static route beats an item route at the same path, so `notes/featured` wins over `notes/[slug]`. WordPress's own paths, like `/wp-admin` and `/wp-json`, cannot be claimed. Every page response is `Cache-Control: private, no-store`, because its HTML depends on the visitor. + +A page reads data with a `load()` function and renders request values through helpers like `` and `orElse()`. Plain `&&` or `?:` on request data is refused at build time, with the line and the helper to use instead. + +## Environment variables + +Read a Space variable with `env()`. Set it with `sf env set`, or put it in `.env.server` and publish. + +```ts +import { action, env } from "zero"; + +const checkout = action({ + access: "anyone", + handler() { + return { configured: env("STRIPE_SECRET_KEY") !== null }; + }, +}); +``` + +See [Environment variables](/environment-variables). + +## Jobs and webhooks + +A job is background work you enqueue from a hook or an action. It runs after the request commits, never inside it, and a job that throws retries with backoff up to its `retries`. + +```ts +jobs: { + notifyEditors: job({ + input: { postId: integer().notNull() }, + retries: 3, + async handler(input, ctx) { + // send the email + }, + }), +}, +``` + +Call `jobs.notifyEditors.enqueue({ postId })` to queue one. A `webhooks` entry sends each matching write to a URL as a POST request, signed in the `zero-signature` header, and retries the same way. On Spacefast a due job can wait up to five minutes before it starts. + +## No realtime + +Zero has no live subscriptions. A client sees a change when it reads again. + +## Before you publish a schema change + +Every version of a Space shares one database, migrated forward to the newest schema. Rolling back the code does not roll back the schema. See [Database](/database#migrations). + +## Run it locally + +```bash +sf zero dev +sf zero dev stop +sf zero build +``` + +`sf zero dev` boots WordPress through WordPress Studio, with no Docker. It needs Node 22 or newer. The first start downloads WordPress and the plugins the app needs. Keep it running and it rebuilds on every save. It prints the local URL and an admin login for local use only. + +`sf zero build` writes the plugins to `.zero/wp-content/`, new migrations to `zero/migrations/`, and the client to `zero/client/`. Commit `zero/migrations/`, `zero/client/`, and `zero.gen.ts`. `.zero/` is local state, and publish skips it. + +`sf zero check` and `sf publish` use the compiler bundled with the CLI. `sf zero dev` and `sf zero build` need the CLI's native optional package. If it is missing, reinstall the CLI with optional dependencies enabled. + +## Bring a schema from another CMS + +```bash +sf zero import payload ../my-payload-app +sf zero import emdash ../site --output app/zero.config.ts +``` + +`sf zero import` reads a Payload, EmDash, or Sanity schema and writes a new `zero.config.ts`. It never runs code from the other CMS and never overwrites a file. Anything Zero cannot hold refuses the import, and nothing is written. Every write rule starts as denied, so set `access` before you publish. + +Have a Zero app on Spacefast with no `zero.config.ts`? See [Migrate an older Zero app](/zero-migrate). + + + + Where the data lives, migrations, and the SQL console. + + + Secrets your actions read with `env()`. + + + Workers, the `functions/` file router, and OpenNext. + + + Every `sf zero` command and its flags. + + diff --git a/content/(reference)/config-file.mdx b/content/(reference)/config-file.mdx index 1e3dbb22..3a3d8ee9 100644 --- a/content/(reference)/config-file.mdx +++ b/content/(reference)/config-file.mdx @@ -52,7 +52,7 @@ sidebar: }, // Run code alongside the files. - "runtime": { "kind": "zero", "server": "server/index.ts", "client": "client/index.tsx" }, + "runtime": { "kind": "functions" }, // Scheduled GETs against this space's own paths. "crons": [{ "path": "/api/digest", "schedule": "daily" }], @@ -121,7 +121,7 @@ While `name` is set in the file, the API refuses to rename the Space. The file w Colors accept hex, `rgb()`, `rgba()`, `hsl()`, `hsla()`, and CSS color names. `theme` is a closed object, so an unknown key is an error. -A `theme.json` file next to `sf.jsonc` is an inference input rather than configuration. Only `settings.color.palette` and `settings.typography.fontFamilies` are read from it. A [Zero](/zero-runtime) capsule's Tailwind compile reads the same file. +A `theme.json` file next to `sf.jsonc` is an inference input rather than configuration. Only `settings.color.palette` and `settings.typography.fontFamilies` are read from it. What these tokens paint, and how to replace those pages outright, is on [Layout, theme, and site pages](/site-pages). @@ -178,16 +178,9 @@ Data location is chosen once, when the Space is created, and never changes. ## Runtime -One runtime per version, discriminated on `kind`. Omit the block for a static site. +Omit the block for a static site. -### `kind: "zero"` - -| Key | Type | Default | What it does | -| --- | --- | --- | --- | -| `runtime.server` | string, required | none | Server entry, relative to the published directory | -| `runtime.client` | string, required | none | Client entry, relative to the published directory | - -Both are required. Zero is never inferred. See [Dynamic sites with Zero](/zero-runtime). +A [Zero](/zero) app needs no `runtime` block. A `zero.config.ts` at the project root is the declaration, and a project that has one must not also set `runtime.kind` to `"zero"`: publish refuses it with `zero_mixed_generation`. ### `kind: "functions"` diff --git a/content/(reference)/glossary.mdx b/content/(reference)/glossary.mdx index 038fb617..0ce0f514 100644 --- a/content/(reference)/glossary.mdx +++ b/content/(reference)/glossary.mdx @@ -15,10 +15,6 @@ A named operation with a schema that a Space's WordPress publishes for agents to A build turns source code into publishable output. You can build locally or let Spacefast build remotely and publish the result. See [Frameworks and builds](/frameworks). -## Capsule - -The compiled form of a Zero app. `sf publish` turns your TypeScript server code into a capsule, which the platform installs on the Space. See [Dynamic sites with Zero](/zero-runtime). - ## Claim / claiming Attaching an anonymous Space (one published with no account) to a real account and team, so it stops counting down to its expiry deadline. See [Publish without an account](/anonymous-and-claim). @@ -53,4 +49,4 @@ An immutable snapshot created when a publish produces a new version. A ready ver ## Zero -The runtime for an app with live queries and a declared database schema. One TypeScript app compiles into a [capsule](#capsule) with a MySQL database next to your code. [Functions](#functions) also provides a database, and [crons](/crons) can call either runtime or static files. See [Dynamic sites with Zero](/zero-runtime). +The compiler that turns one `zero.config.ts` (collections, tables, actions, and pages) into a WordPress app, hosted on the Space with its own MySQL database. [Functions](#functions) also provides a database, and [crons](/crons) can call either runtime or static files. See [Dynamic sites with Zero](/zero). diff --git a/content/(serve)/site-pages.mdx b/content/(serve)/site-pages.mdx index e7b409a9..c90bf42e 100644 --- a/content/(serve)/site-pages.mdx +++ b/content/(serve)/site-pages.mdx @@ -59,7 +59,7 @@ A WordPress-compatible `theme.json` next to your config is read as an inference Every other WordPress key rides through and is never read. Slugs are lowercase letters, digits, and dashes, up to 64 characters. The palette takes at most 64 entries and `fontFamilies` at most 32. The file is read up to 64 KiB at publish; a larger one is ignored rather than failing the publish. -`theme.json` is the weakest of the three theme sources. What `sf.jsonc` sets under `theme` overrides it, and what the Space's [Customization](/customization) tab sets overrides both. A [Zero](/zero-runtime) capsule's Tailwind compile reads the same file, so one file styles both. +`theme.json` is the weakest of the three theme sources. What `sf.jsonc` sets under `theme` overrides it, and what the Space's [Customization](/customization) tab sets overrides both. ## `_layout.html` diff --git a/content/cli/builds.mdx b/content/cli/builds.mdx index 3447e50f..88aac8ca 100644 --- a/content/cli/builds.mdx +++ b/content/cli/builds.mdx @@ -17,7 +17,7 @@ Build and pack project output. sf build [DIR] ``` -Detects build settings, runs the build, and packs the output into an archive. A `zero` runtime compiles the capsule instead and leaves the app shell that `sf publish` would upload. This command talks to your filesystem, not to a Space. +Detects build settings, runs the build, and packs the output into an archive. This command talks to your filesystem, not to a Space. To compile a [Zero](/zero) app locally, use `sf zero build`. ### Arguments diff --git a/content/cli/db.mdx b/content/cli/db.mdx index b82a52e8..332d12ce 100644 --- a/content/cli/db.mdx +++ b/content/cli/db.mdx @@ -1,170 +1,21 @@ --- title: db and crons -description: Inspect a Space's database, dump or export rows, apply schema migrations, open a SQL console, and exercise scheduled jobs +description: Open a single-use SQL console on a Space's database, and list and run its scheduled jobs sidebar: order: 22 --- -Inspect, migrate, and dump a Zero Space's database without a connection string. Functions workers use `env.DB` or the SQL console instead of the Zero table commands. You can also trigger a declared cron job on demand. +Open a single-use SQL console on a Space's database, and exercise its scheduled jobs on demand. A [Zero](/zero) app's schema lives in its `zero.config.ts` and migrates on publish. See [Database](/database). Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`) and the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). -`sf db`, `sf db dump`, and `sf db migrate` are Zero capsules only. A Functions worker declares no table set, so those routes answer `db_not_available` or `runtime_kind_mismatch`. There is no connection string anywhere in the CLI or the API. `sf db console` is the escape hatch. +There is no connection string anywhere in the CLI or the API. `sf db console` is how you reach the database directly. -## sf db - -Inspect a Space's database. Unlike other topic commands, the bare `sf db` runs the inspect read instead of printing help. - -```text -sf db [target] [--space ] [--local-url ] [--port ] -``` - -### Arguments - -| Argument | What it is | -| --- | --- | -| `[target]` | Space ID, slug, live URL, or domain. Defaults to the linked Space | - -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--local-url=` | none | Read from a local `sf dev` server instead of the live version. Must be `http:` or `https:` with hostname `localhost`, `127.0.0.1`, `::1`, or `[::1]` | -| `--port=` | none | Local `sf dev` server port. Shorthand for `--local-url http://127.0.0.1:`. Integer 1 to 65535 | - -```bash -sf db -sf db docs -sf db --port 4173 -``` - -### Output - -Tables, columns, and the pending migration plan. With `--json`, `data` carries `versionId`, `artifactId`, `backend` (always `mysql`), `migrationMode` (`safe`, `drop`, or `rename`), `schemaHash`, `tables[]`, `migrations[]`, and `plan`. `plan.applied` is `null` when the control plane cannot prove the plan ran. - -### Errors - -| Code | Exit | When | -| --- | --- | --- | -| `network_error` | 40 | A local `sf dev` port refused the connection | - -## sf db dump - -Print rows from a Space's database, for debugging and export. - -```text -sf db dump [target] [--space ] [--table ] [--limit ] - [--local-url ] [--port ] -``` - -### Arguments - -| Argument | What it is | -| --- | --- | -| `[target]` | Space ID, slug, live URL, or domain. Defaults to the linked Space | - -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--table=` | none | Restrict the dump to one table. Must match `^[A-Za-z_][A-Za-z0-9_]*$`, 1 to 128 characters | -| `--limit=` | `25` | Maximum rows per table. Maximum 100. Validated before the request | -| `--local-url=` | none | Read from a local `sf dev` server instead of the live version | -| `--port=` | none | Local `sf dev` server port | - -```bash -sf db dump -sf db dump --table todos --limit 100 -``` - -### Output - -With `--json`, `data` is `{ versionId, artifactId, schemaHash, tables: [{ name, rows }] }`. - -## sf db export - -Export every declared table to one versioned JSON backup. Rows are read in stable keyset pages and the destination is replaced only after the complete export succeeds. - -```text -sf db export [target] [--space ] [--out ] [--local-url ] [--port ] -``` - -### Arguments - -| Argument | What it is | -| --- | --- | -| `[target]` | Space ID, slug, live URL, or domain. Defaults to the linked Space | - -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--out=` | `spacefast-backup--.json` | Backup file path | -| `--local-url=` | none | Read from a local `sf dev` server instead of the live version | -| `--port=` | none | Local `sf dev` server port | - -The file is written to a temp path with mode `0600`, fsynced, then renamed. A failed export leaves an existing destination untouched and unlinks the temp file. - -```bash -sf db export -sf db export docs --out ./docs-backup.json -sf db export --port 4173 -``` - -### Output - -With `--json`, `data` is `{ spacefastBackup: 1, path, spaceId, versionId, exportedAt, schemaHash, tableCount, rowCount }`. - -### Errors - -| Code | Exit | When | -| --- | --- | --- | -| `zero_db_export_cursor_repeated` | 1 | A page returned the same cursor twice | - -## sf db migrate - -Apply the live version's schema migrations, and report what this source tree would change on top of them. - -```text -sf db migrate [source] [--space ] [--previous-artifact ] [--drop] [--rename] -``` - -### Arguments - -| Argument | What it is | -| --- | --- | -| `[source]` | Capsule source directory. Defaults to `.` | - -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--previous-artifact=` | none | Diff against this artifact or finalize payload instead of the live schema | -| `--drop` | `false` | Allow the planned migration to include explicit drop operations. Exclusive with `--rename` | -| `--rename` | `false` | Allow the planned migration to include explicit rename operations. Exclusive with `--drop` | - -`create_table`, `add_column`, and `add_index` are always allowed. `drop_table`, `drop_column`, and `drop_index` need `--drop`. `rename_table` and `rename_column` need `--rename`. - -The command checks the runtime kind locally before it sends anything, and confirms before applying. - -```bash -sf db migrate -sf db migrate --drop -``` - -### Output - -With `--json`, `data` is `{ plan, applied, operation }`. - -### Errors - -| Code | Exit | When | -| --- | --- | --- | -| `runtime_kind_mismatch` | 30 | `sf.jsonc` declares a runtime that is not a Zero capsule. Raised before any request | +An older Zero app that declares `"runtime": { "kind": "zero" }` in `sf.jsonc` also has `sf db`, `sf db list`, `sf db dump`, `sf db export`, and `sf db migrate`. See [Migrate an older Zero app](/zero-migrate#read-and-migrate-its-database). ## sf db console -Open a phpMyAdmin console on the Space's database, for anything `sf db` and `sf db dump` cannot answer. +Open a phpMyAdmin console on the Space's database. ```text sf db console [target] [--space ] [--show-secret] diff --git a/content/cli/project.mdx b/content/cli/project.mdx index 2b1df5b8..d6fb435d 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -30,11 +30,8 @@ sf init [NAME] [--space ] [-o ] [-f] [--runtime static|zero|functio | Flag | Default | What it does | | --- | --- | --- | | `-f, --force` | `false` | Overwrite existing generated files. | -| `--runtime=` | `static` | Project runtime to scaffold. | -| `--template=` | none | Zero starter template to scaffold. | -| `--title=` | none | Zero starter app title. | -| `--no-git` | off | Skip initializing git for a Zero starter app. | -| `--space=` | none | Space ID, live URL, domain, or slug to link the directory to. Env `SPACEFAST_SPACE`. | +| `--runtime=` | `static` | Project to scaffold. `zero` writes a starter [Zero](/zero) `zero.config.ts`, the same one `sf zero init` writes. | +| `--space=` | none | Space ID, live URL, domain, or slug to link the directory to. Only with `--runtime static`. Env `SPACEFAST_SPACE`. | | `-o, --team=` | default team | Team slug, ID, or name. Env `SPACEFAST_TEAM`. | | `--claim-token=` | none | Space key (`sfc_…`) for an unclaimed anonymous Space. Env `SPACEFAST_CLAIM_TOKEN`. | @@ -44,7 +41,7 @@ sf init [NAME] [--space ] [-o ] [-f] [--runtime static|zero|functio sf init sf init my-site sf init --space docs -sf init my-app --runtime zero --template guestbook +sf init my-app --runtime zero ``` ### What it writes diff --git a/content/cli/publish.mdx b/content/cli/publish.mdx index 30f90323..cdd183a0 100644 --- a/content/cli/publish.mdx +++ b/content/cli/publish.mdx @@ -295,17 +295,14 @@ Start the local dev server. sf dev ``` -`sf dev` runs the project in this directory. It is not a local mirror of what publish serves: for a `zero` runtime it starts the capsule dev server, and for anything else it serves a Pages preview with sample data and the publish-time expander at `/_spacefast/pages/`. There is no static file server for your site on that path. For a framework project, use that framework's own dev server (`next dev`, `vite`), then run `sf publish` when you're ready. +In a Pages project, `sf dev` serves a Pages preview with sample data and the publish-time expander at `/_spacefast/pages/`. It is not a local mirror of what publish serves, and there is no static file server for your site on that path. For a [Zero](/zero) app, run `sf zero dev`. An older Zero app that declares `"runtime": { "kind": "zero" }` in `sf.jsonc` gets its own Zero 0.8 dev server from `sf dev` instead of a Pages preview. See [Migrate an older Zero app](/zero-migrate#keep-working-on-a-08-app). For a framework project, use that framework's own dev server (`next dev`, `vite`), then run `sf publish` when you're ready. | Flag | Default | What it does | | --- | --- | --- | -| `-d, --dir=` | `.` | Project directory | -| `-p, --port=` | `4173` | Preview port. 1 to 65535 | +| `--dir=` | `.` | Project directory | +| `--port=` | `4173` | Preview port. 1 to 65535 | | `--host=` | `127.0.0.1` | Host interface for the local dev server | | `--allow-network` | off | Allow an explicit non-loopback binding | -| `--state-backend=memory\|sqlite` | `memory` | Local state adapter for a runtime dev server | -| `--watch` / `--no-watch` | `--watch` | Poll source files and reload the runtime after changes | -| `--watch-interval=` | `1000` | Source polling interval in milliseconds | | `--dry-run` | off | Print the dev server plan without starting it | A non-loopback `--host` without `--allow-network` fails with `validation_error`. A project whose `sf.jsonc` declares the `functions` runtime fails with `runtime_dev_unsupported` (exit 2), which points you at your framework's dev server. diff --git a/content/cli/storage.mdx b/content/cli/storage.mdx index bb4d82f7..adfd93a2 100644 --- a/content/cli/storage.mdx +++ b/content/cli/storage.mdx @@ -1,11 +1,11 @@ --- title: storage and logs -description: List and delete the objects a Space's app uploaded, and read the request and handler logs the Space produced +description: Upload, list, and delete the objects in a Space's app storage, and read the request and handler logs the Space produced sidebar: order: 23 --- -List your app's stored objects and read access, edge, or runtime logs within your plan's retention window. Runtime lines take time to appear, and static Spaces have no runtime logs. +Upload, list, and delete your app's stored objects, and read access, edge, or runtime logs within your plan's retention window. Runtime lines take time to appear, and static Spaces have no runtime logs. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`) and the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). @@ -13,7 +13,7 @@ Every command here takes the global flags (`--api-url`, `--profile`, `--token`, List object metadata and quota usage for files uploaded by a Space's app. -This is runtime object storage, not your published files. Only a Space with a runtime app has any. Objects are addressed by a bare 32-character hex id. There is no key and no per-object visibility. +This is runtime object storage, not your published files. Only a Space with a runtime app has any. Objects are addressed by a bare 32-character hex id. There is no key or path. ```text sf storage [target] [--space ] [--limit ] [--cursor ] @@ -46,7 +46,7 @@ A table of `Id | Size | Type | Uploaded by | Created`, or "No stored objects." A With `--json`, `data` is `{ objects, cursor }`. Each object carries `id`, `contentType`, `createdAt`, `filename`, `size`, `uploaderId`, `sha256`, and `url`. -There is no `sf storage upload`. Uploading is an API call, or `storage.upload()` from the client SDK inside your app. +Upload with [`sf storage put`](#sf-storage-put) or the API. ### Errors @@ -54,6 +54,44 @@ There is no `sf storage upload`. Uploading is an API call, or `storage.upload()` | --- | --- | --- | | `validation_error` | 2 | `--limit` or `--cursor` did not parse. Raised before the request | +## sf storage put + +Upload one file to the Space's app storage. + +```text +sf storage put [--space ] [--content-type ] [--public] +``` + +### Arguments + +| Argument | What it is | +| --- | --- | +| `` | Path to a regular file. Required. 5 MiB at most | + +### Flags + +| Flag | Default | What it does | +| --- | --- | --- | +| `--content-type=` | `application/octet-stream` | The object's MIME type | +| `--public` | `false` | Make the object readable by anyone holding its URL | + +The file's base name becomes the object's `filename`. Without `--public` the object is private, and the public read URL never serves it. The same content policy as any upload applies, so active web content and executables are refused. See [Storage](/object-storage#limits). + +```bash +sf storage put receipt.pdf --content-type application/pdf +sf storage put logo.png --content-type image/png --public --space docs +``` + +### Output + +`Uploaded : ( B).` With `--json`, `data` is the stored object. + +### Errors + +| Code | Exit | When | +| --- | --- | --- | +| `validation_error` | 2 | The path is not a regular file, or the file is larger than 5 MiB. Raised before the request | + ## sf storage rm Force-delete an app storage object as the Space owner, regardless of who uploaded it. Aliased as `sf storage remove` and `sf storage delete`. @@ -110,7 +148,7 @@ A bare kind in the first slot works: `sf logs runtime` is the same as `sf logs - | `-f, --follow` | `false` | Poll for new entries every 2 seconds until interrupted with Ctrl-C | | `--cursor=` | none | Continue from the cursor printed by the previous page | | `--request-id=` | none | Runtime logs only. Everything logged while serving one request | -| `--handler=` | none | Runtime logs only. Lines from one Zero mutation or Functions handler | +| `--handler=` | none | Runtime logs only. Lines from one Functions handler | `--follow` polls, it does not stream over a socket. It dedupes by the whole entry and remembers up to 5,000 keys. diff --git a/content/cli/zero.mdx b/content/cli/zero.mdx index 5281ee4e..2a851691 100644 --- a/content/cli/zero.mdx +++ b/content/cli/zero.mdx @@ -1,139 +1,145 @@ --- title: zero, wp, runtime, pages, design, analytics -description: Inspect and call a Space's Zero Abilities, generate types, run WP-CLI, see what a Space is serving, and read its traffic +description: Start, check, run, and build Zero apps, import a CMS schema, run WP-CLI, see what a Space is serving, and read its traffic sidebar: order: 25 --- -List a live capsule's published Abilities and call the ones your credential permits. Remote `sf wp` needs a provisioned Space and write access, and reports status without the command's printed output. +`sf zero` builds and runs [Zero](/zero) apps: a `zero.config.ts` compiled to WordPress. The rest of this page covers WP-CLI, runtime status, site pages, design tokens, and analytics. -Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`). All except `sf zero import`, `sf pages pull`, `sf pages validate`, and `sf design generate` also take the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). +Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`). `sf wp`, `sf runtime status`, and `sf analytics` also take the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). -## sf zero abilities +To publish a Zero app, run `sf publish` from its root. See [Dynamic sites with Zero](/zero). -List the Abilities the Space's live capsule publishes: what an agent can call, what category it belongs to, and whether it reads or writes. Aliased as `sf zero ls`. +## sf zero init + +Start a Zero app: a starter `zero.config.ts` with one collection, one table, and one action, named after the directory. ```text -sf zero abilities [--space ] +sf zero init [name] [-f] ``` -No arguments, no command-specific flags. +| Argument or flag | What it does | +| --- | --- | +| `[name]` | Directory to create. Omit to initialize the current directory | +| `-f, --force` | Overwrite an existing `zero.config.ts` | + +It also adds `.zero/` and `node_modules/` to `.gitignore`. `sf init --runtime zero` writes the same starter. ```bash -sf zero abilities +sf zero init my-app +sf zero init ``` -### Output +## sf zero check + +Compile `zero.config.ts` and report diagnostics without writing anything. + +```text +sf zero check [project] [--json] +``` -A table of `ABILITY | CATEGORY | MODE | REQUIRES`, or "This space publishes no Zero Abilities." The `REQUIRES` column is the capability or Grant capabilities the Ability needs, which is what answers "why did that come back 403". +Each diagnostic names a `file:line:col` and a fix. `--json` prints the diagnostics envelope instead of text. `sf zero check` uses the compiler bundled with the CLI, so it needs no native download. -With `--json`, `data` is the array of Ability rows. +```bash +sf zero check +sf zero check ./app --json +``` -## sf zero call +## sf zero dev -Mint a short-lived Ability token for the Space and run the Ability against the Space's own WordPress REST API. The token is never printed and never appears in the `--json` receipt. +Run the app on a local WordPress and rebuild when its source changes. ```text -sf zero call [--space ] [--credential ] [--input ] +sf zero dev [project] [--port ] [--no-watch] [--allow-destructive] +sf zero dev stop [project] +sf zero dev status [project] +sf zero dev wp [project] -- ``` -### Arguments +`sf zero dev` boots WordPress through WordPress Studio, with no Docker. It needs Node 22 or newer, and the first start downloads WordPress and the plugins the app needs. It watches TypeScript, config, and `public/` files. Ctrl-C stops WordPress and keeps the local database for the next start. -| Argument | What it is | +| Argument or flag | What it does | | --- | --- | -| `` | Ability name, as shown by `sf zero abilities`. Required | +| `--port=` | Port for a new local site. An existing site keeps its port | +| `--no-watch` | Start the site and return, without watching | +| `--allow-destructive` | Apply a schema change that drops or retypes data, once. Saves after that are protected again | +| `stop` | Stop the local site | +| `status` | Print whether the local site is running, and its URL | +| `wp -- ` | Run WP-CLI against the local site | -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--credential=` | the only one, when there is exactly one | Machine credential id to mint the Ability token for | -| `--input=` | none | Ability input as inline JSON, or `@path` to read it from a file | - -The minted token travels in `X-SF-Authorization`. The plain `Authorization` header belongs to your application and passes through untouched. +The login it prints is for local development only. ```bash -sf zero call content.posts.list -sf zero call content.posts.get --input '{"slug":"hello"}' -sf zero call content.posts.save --input @post.json +sf zero dev +sf zero dev --no-watch +sf zero dev wp -- user application-password create admin local --porcelain +sf zero dev stop ``` -### Output +## sf zero build -The HTTP status, then the Ability's JSON result. With `--json`, `data` is `{ ability, spaceId, endpoint, credentialId, role, status, ok, result }`. +Compile `zero.config.ts` into WordPress plugins. -### Errors +```text +sf zero build [project] [--out ] +``` -| Code | Exit | When | -| --- | --- | --- | -| `zero_ability_failed` | 1 | The Ability answered a non-2xx status. The whole receipt rides along in `details`. A 403 means the credential's Grant does not permit this Ability | +It writes the plugins to `.zero/wp-content/`, new migrations to `zero/migrations/`, the typed client to `zero/client/`, and editor types to `zero.gen.ts`. Commit everything except `.zero/`. -## sf zero types +```bash +sf zero build +sf zero build ./app --out ./dist +``` -Generate TypeScript for everything the platform owns rather than your source: the live content model's types, the Ability catalog with the capability each one needs, and a row type per database table. Commit the result and use `--check` in CI. +`sf zero dev` and `sf zero build` run the native compiler from the CLI's optional platform package. If it's missing, reinstall the CLI with optional dependencies enabled. Set `SPACEFAST_ZERO_BIN` to a `zero` binary to use your own build instead. + +## sf zero import + +Translate a Payload, EmDash, or Sanity schema into a new `zero.config.ts`, check that it compiles, and print what mapped and what did not. Fully local, no space flags. ```text -sf zero types [--space ] [--out ] [--check] +sf zero import [--output ] ``` -### Flags +| Argument or flag | What it does | +| --- | --- | +| `` | `payload`, `emdash`, or `sanity`. `payloadcms` is accepted for `payload` | +| `` | The schema file, or the project directory to find it in (`payload.config.ts`, `.emdash/seed.json`, `sanity.config.ts`) | +| `--output=` | Config file to write. Defaults to `zero.config.ts`, and it must not exist yet | -| Flag | Default | What it does | -| --- | --- | --- | -| `--out=` | `zero-types.ts` | File to write, relative to the current directory | -| `--check` | `false` | Compare instead of writing, and exit non-zero when the file is stale or missing | +It never runs code from the other CMS and never overwrites a file. Anything Zero cannot hold refuses the whole import, and nothing is written. Every write rule in the result starts as denied, so set `access` before you publish. ```bash -sf zero types -sf zero types --check +sf zero import payload ../my-payload-app +sf zero import emdash ../site --output app/zero.config.ts ``` ### Output -With `--json`, `data` is `{ out, spaceId, versionId, contentModelRevision, abilities, tables, checked, drifted }`. - -### Errors - -| Code | Exit | When | -| --- | --- | --- | -| `zero_types_stale` | 2 | `--check` and the file is missing or no longer matches what the Space publishes | - -## sf zero import +One line per schema entry: its outcome, its JSON pointer, and the reason. With `--json`, `data` carries `source`, `from`, `out`, and `report.entries[]`. -Translate a Payload CMS or EmDash project into an authored Zero capsule and its content files, and print the translation report. This is a local codegen step with no space flags. +## sf zero migrate -A Payload config carrying anything Zero cannot hold is refused outright, and nothing is written. A content model missing the fields that refused would publish a site that silently lost content. +Bring a schema, a site's content, or an older Zero app into a `zero.config.ts` project. Everything after `migrate` passes to the Zero compiler unchanged. ```text -sf zero import [--out ] [--capsule ] [--force] +sf zero migrate config --from --out +sf zero migrate site [--dry-run] +sf zero migrate app ... ``` -### Arguments - -| Argument | What it is | +| Subcommand | What it does | | --- | --- | -| `` | Project format to translate: `payloadcms` or `emdash`. Required | -| `` | Directory holding the project to translate. Required | - -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--out=` | current directory | Directory to write the capsule and content into | -| `--capsule=` | `server/index.ts` | Capsule module to write, relative to `--out` | -| `--force` | `false` | Overwrite files the import would otherwise refuse to replace | +| `config` | The same schema import as [`sf zero import`](#sf-zero-import), with its own flags | +| `site` | Moves an older Zero site's content into the app's collections on a standalone WordPress, through WP-CLI. It does not touch a Space. `--dry-run` previews it. Needs the native compiler, like `sf zero build` | +| `app` | Converts an older Zero app's source. See [Migrate an older Zero app](/zero-migrate) | ```bash -sf zero import payloadcms ../my-payload-app -sf zero import emdash ../my-emdash-site --out . +sf zero migrate config --from payload payload.config.ts --out app/zero.config.ts +sf zero migrate site content-model.php --dry-run ``` -### Errors - -| Code | Exit | When | -| --- | --- | --- | -| `zero_import_refused` | 1 | The source config carries something Zero cannot hold. Nothing is written | - ## sf wp Run WP-CLI against a Space's WordPress, or against a local one with `--local`. @@ -169,7 +175,7 @@ sf wp -- --version Show what a Space is serving: its live version, the URL it answers on, and what the live version deployed when it ships code. -A capsule lists tables, queries, mutations, endpoints, migration count, and schema digest. A worker lists entry, bundle digest, routes, endpoints, and capabilities. All of it comes from version metadata, so it answers while the runtime is asleep or moving. +A worker lists entry, bundle digest, routes, endpoints, and capabilities. All of it comes from version metadata, so it answers while the runtime is asleep or moving. ```text sf runtime status [--space ] @@ -193,7 +199,7 @@ Live URL: https://docs.view.fast Then one table per runtime section. A static Space has no app and prints nothing further. -With `--json`, `data` carries `liveVersionId`, `pendingVersionId`, `liveUrl`, `runtimeState`, `liveVersionStatus`, `pendingVersionStatus`, `app` (with a `capsule` or a `worker`), and `operation`. +With `--json`, `data` carries `liveVersionId`, `pendingVersionId`, `liveUrl`, `runtimeState`, `liveVersionStatus`, `pendingVersionStatus`, `app`, and `operation`. ## sf pages pull @@ -245,7 +251,7 @@ The command sets exit code 2 when any diagnostic has severity `error`. Warnings Read `DESIGN.md`'s ` ```theme.json ` and ` ```layout.html ` fenced blocks and write `theme.json` plus `_layout.html`. Fully local, no API call. -`theme.json` can seed Pages tokens and feeds a Zero capsule's Tailwind compile. `_layout.html` provides site chrome. `DESIGN.md` itself is never read at finalize or serve time; this is an authoring-time codegen step. +`theme.json` can seed Pages tokens. `_layout.html` provides site chrome. `DESIGN.md` itself is never read at finalize or serve time; this is an authoring-time codegen step. ```text sf design generate [-f] diff --git a/content/index.mdx b/content/index.mdx index 5cceb0ec..40ccab2f 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -41,7 +41,7 @@ Publish this folder to Spacefast and give me the live URL. A **Space** holds a site and its **versions**, immutable snapshots with their own URLs. A publish with no changes can reuse the current version. The `live` channel points at the version visitors see, or is empty before the first publish. Promoting or rolling back to a retained, ready version moves that pointer without rebuilding. Preview publishes and manual promotion policies leave `live` alone. (New terms piling up? See the [glossary](/glossary).) -Static output is the default path. Point `sf publish` at a build directory and it uploads only the files that changed. If you'd rather publish source, Spacefast detects the framework and builds it. Sites that need a database, server functions, or scheduled jobs declare a [Zero runtime](/zero-runtime#declare-it) and server entry. +Static output is the default path. Point `sf publish` at a build directory and it uploads only the files that changed. If you'd rather publish source, Spacefast detects the framework and builds it. Sites that need a database, server functions, or scheduled jobs add a [Zero](/zero) `zero.config.ts` or a [Functions](/functions) worker. In a self-serve account, your team owns the Spaces you claim, even when you are its only member. On [Plus](/billing#seats), you can add members who publish and roll back without sharing your login. Free and Go allow only the owner. Team roles control who can manage domains, credentials, and billing. See [Teams and members](/teams) to invite people and choose how new Spaces are shared. @@ -53,7 +53,7 @@ In a self-serve account, your team owns the Spaces you claim, even when you are Drag a folder onto drop.fast. No install, no account required. - Space, version, live, capsule, Ability, grant — core terms in one place. + Space, version, live, Ability, grant — core terms in one place. What gets uploaded, how versions work, and how to roll back. diff --git a/content/infrastructure/server-specs-settings.mdx b/content/infrastructure/server-specs-settings.mdx index 8e9c6305..9184891a 100644 --- a/content/infrastructure/server-specs-settings.mdx +++ b/content/infrastructure/server-specs-settings.mdx @@ -25,7 +25,7 @@ For application access to WordPress, use the documented [WordPress commands and | --- | --- | | Redirects, rewrites, and response headers | Publish `_redirects`, `_headers`, or the supported [routing configuration](/routing). | | Application environment variables | Use [Environment variables](/environment-variables). | -| Runtime configuration | Use the documented [Zero runtime configuration](/zero-runtime). | +| Runtime configuration | Use the documented [Functions runtime settings](/functions) or [Zero app configuration](/zero). | | Storage and resource entitlements | Check [Limits](/limits). | Spacefast rejects authored `.htaccess` and `.user.ini` files. You cannot replace the managed server configuration or `php.ini` through a publish. Use the supported configuration for the behavior you need. diff --git a/content/quickstart.mdx b/content/quickstart.mdx index 9f9819bb..a5b4f63d 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -107,8 +107,8 @@ Working with other people? If your Space is still anonymous, [claim it first](/a Push to a remote or connect GitHub and let Spacefast build. - - Add a database, functions, and crons with the Zero runtime. + + Add collections, tables, and actions with Zero. `CI=1` switches the CLI to JSON output and no prompts. diff --git a/evals.yaml b/evals.yaml index c649d5ab..f31f7358 100644 --- a/evals.yaml +++ b/evals.yaml @@ -70,8 +70,8 @@ questions: routes: /config-file - id: zero-declare question: How do I turn on the Zero runtime for a project? - expected: ["runtime.kind set to zero in sf.jsonc", "a server entry"] - routes: /zero-runtime + expected: ["a zero.config.ts at the project root", "no runtime block in sf.jsonc"] + routes: /zero - id: routing-precedence question: If both a _redirects file and sf.jsonc rules match a path, which wins? expected: ["_redirects"] diff --git a/generated/cli/index.md b/generated/cli/index.md index f29ae69a..74c625c0 100644 --- a/generated/cli/index.md +++ b/generated/cli/index.md @@ -20,7 +20,7 @@ Tell your agent to pick the smallest lane that does the job: $ npm install -g spacefast $ sf COMMAND running command... -$ sf (--version) +$ sf (--version|-v) spacefast/0.8.1 $ sf --help [COMMAND] USAGE @@ -48,15 +48,15 @@ GLOBAL FLAGS ## Commands - [`sf access`](#sf-access) -- [`sf access logout-all`](#sf-access-logout-all) +- [`sf access sessions revoke`](#sf-access-sessions-revoke) - [`sf activity`](#sf-activity) - [`sf agents init [DIRECTORY]`](#sf-agents-init-directory) - [`sf analytics`](#sf-analytics) -- [`sf api METHODORPATH [PATH]`](#sf-api-methodorpath-path) +- [`sf api METHOD [PATH]`](#sf-api-method-path) - [`sf api-keys`](#sf-api-keys) - [`sf api-keys create`](#sf-api-keys-create) - [`sf api-keys list`](#sf-api-keys-list) -- [`sf api-keys revoke ID`](#sf-api-keys-revoke-id) +- [`sf api-keys revoke KEY`](#sf-api-keys-revoke-key) - [`sf apply`](#sf-apply) - [`sf auth`](#sf-auth) - [`sf auth as NAME`](#sf-auth-as-name) @@ -64,23 +64,23 @@ GLOBAL FLAGS - [`sf autocomplete [SHELL]`](#sf-autocomplete-shell) - [`sf build [DIR]`](#sf-build-dir) - [`sf builds cancel BUILD`](#sf-builds-cancel-build) -- [`sf builds detect`](#sf-builds-detect) -- [`sf builds get BUILD`](#sf-builds-get-build) +- [`sf builds detect [DIR]`](#sf-builds-detect-dir) +- [`sf builds inspect BUILD`](#sf-builds-inspect-build) +- [`sf builds list`](#sf-builds-list) - [`sf builds logs BUILD`](#sf-builds-logs-build) -- [`sf builds ls`](#sf-builds-ls) -- [`sf builds refresh-upload BUILD`](#sf-builds-refresh-upload-build) - [`sf builds retry BUILD`](#sf-builds-retry-build) +- [`sf builds upload refresh BUILD`](#sf-builds-upload-refresh-build) - [`sf channels`](#sf-channels) - [`sf channels history [NAME]`](#sf-channels-history-name) -- [`sf channels ls`](#sf-channels-ls) +- [`sf channels list`](#sf-channels-list) - [`sf channels set NAME`](#sf-channels-set-name) - [`sf comments`](#sf-comments) - [`sf comments create`](#sf-comments-create) - [`sf comments export`](#sf-comments-export) -- [`sf comments get COMMENT`](#sf-comments-get-comment) +- [`sf comments inspect COMMENT`](#sf-comments-inspect-comment) - [`sf comments list`](#sf-comments-list) -- [`sf comments mark-read COMMENT`](#sf-comments-mark-read-comment) - [`sf comments react COMMENT EMOJI`](#sf-comments-react-comment-emoji) +- [`sf comments read COMMENT`](#sf-comments-read-comment) - [`sf comments reopen COMMENT`](#sf-comments-reopen-comment) - [`sf comments reply COMMENT`](#sf-comments-reply-comment) - [`sf comments resolve COMMENT`](#sf-comments-resolve-comment) @@ -88,35 +88,35 @@ GLOBAL FLAGS - [`sf comments settings set`](#sf-comments-settings-set) - [`sf comments unreact COMMENT EMOJI`](#sf-comments-unreact-comment-emoji) - [`sf connectors`](#sf-connectors) -- [`sf connectors add TARGET`](#sf-connectors-add-target) - [`sf connectors bind ROLE CONNECTION`](#sf-connectors-bind-role-connection) - [`sf connectors bindings`](#sf-connectors-bindings) - [`sf connectors connections`](#sf-connectors-connections) -- [`sf connectors connections add CONNECTOR`](#sf-connectors-connections-add-connector) -- [`sf connectors connections edit CONNECTION`](#sf-connectors-connections-edit-connection) +- [`sf connectors connections connect CONNECTOR`](#sf-connectors-connections-connect-connector) +- [`sf connectors connections disconnect CONNECTION`](#sf-connectors-connections-disconnect-connection) - [`sf connectors connections health CONNECTION`](#sf-connectors-connections-health-connection) -- [`sf connectors connections ls`](#sf-connectors-connections-ls) +- [`sf connectors connections list`](#sf-connectors-connections-list) - [`sf connectors connections reconnect CONNECTION`](#sf-connectors-connections-reconnect-connection) - [`sf connectors connections refresh CONNECTION`](#sf-connectors-connections-refresh-connection) -- [`sf connectors connections rm CONNECTION`](#sf-connectors-connections-rm-connection) +- [`sf connectors connections update CONNECTION`](#sf-connectors-connections-update-connection) +- [`sf connectors create TARGET`](#sf-connectors-create-target) +- [`sf connectors delete CONNECTOR`](#sf-connectors-delete-connector) - [`sf connectors detect URL`](#sf-connectors-detect-url) -- [`sf connectors ls`](#sf-connectors-ls) +- [`sf connectors list`](#sf-connectors-list) - [`sf connectors policies`](#sf-connectors-policies) - [`sf connectors policies add PATTERN`](#sf-connectors-policies-add-pattern) - [`sf connectors policies explain ADDRESS`](#sf-connectors-policies-explain-address) -- [`sf connectors policies ls`](#sf-connectors-policies-ls) -- [`sf connectors policies rm RULE`](#sf-connectors-policies-rm-rule) +- [`sf connectors policies list`](#sf-connectors-policies-list) +- [`sf connectors policies remove RULE`](#sf-connectors-policies-remove-rule) - [`sf connectors policies set RULE`](#sf-connectors-policies-set-rule) -- [`sf connectors rm CONNECTOR`](#sf-connectors-rm-connector) - [`sf connectors run [CODE]`](#sf-connectors-run-code) -- [`sf connectors run get RUN`](#sf-connectors-run-get-run) +- [`sf connectors run inspect RUN`](#sf-connectors-run-inspect-run) - [`sf connectors run list`](#sf-connectors-run-list) - [`sf connectors run resume RUN`](#sf-connectors-run-resume-run) - [`sf connectors unbind ROLE`](#sf-connectors-unbind-role) - [`sf content dashboard [TARGET]`](#sf-content-dashboard-target) - [`sf continue`](#sf-continue) - [`sf crons`](#sf-crons) -- [`sf crons ls`](#sf-crons-ls) +- [`sf crons list`](#sf-crons-list) - [`sf crons run TARGET`](#sf-crons-run-target) - [`sf db [TARGET]`](#sf-db-target) - [`sf db console [TARGET]`](#sf-db-console-target) @@ -127,7 +127,7 @@ GLOBAL FLAGS - [`sf design`](#sf-design) - [`sf design generate`](#sf-design-generate) - [`sf dev`](#sf-dev) -- [`sf dev run-many`](#sf-dev-run-many) +- [`sf dev instances`](#sf-dev-instances) - [`sf docs [QUERY]`](#sf-docs-query) - [`sf doctor`](#sf-doctor) - [`sf domains`](#sf-domains) @@ -135,36 +135,36 @@ GLOBAL FLAGS - [`sf domains check DOMAIN`](#sf-domains-check-domain) - [`sf domains contacts`](#sf-domains-contacts) - [`sf domains contacts add`](#sf-domains-contacts-add) -- [`sf domains contacts ls`](#sf-domains-contacts-ls) +- [`sf domains contacts list`](#sf-domains-contacts-list) - [`sf domains diagnostics DOMAIN`](#sf-domains-diagnostics-domain) - [`sf domains dns`](#sf-domains-dns) - [`sf domains dns add DOMAIN`](#sf-domains-dns-add-domain) - [`sf domains dns batch DOMAIN`](#sf-domains-dns-batch-domain) - [`sf domains dns capabilities DOMAIN`](#sf-domains-dns-capabilities-domain) - [`sf domains dns export DOMAIN`](#sf-domains-dns-export-domain) -- [`sf domains dns ls DOMAIN`](#sf-domains-dns-ls-domain) +- [`sf domains dns list DOMAIN`](#sf-domains-dns-list-domain) - [`sf domains dns refresh DOMAIN`](#sf-domains-dns-refresh-domain) +- [`sf domains dns remove DOMAIN RECORD`](#sf-domains-dns-remove-domain-record) - [`sf domains dns replace DOMAIN`](#sf-domains-dns-replace-domain) -- [`sf domains dns rm DOMAIN RECORD`](#sf-domains-dns-rm-domain-record) - [`sf domains dns update DOMAIN RECORD`](#sf-domains-dns-update-domain-record) -- [`sf domains ls`](#sf-domains-ls) +- [`sf domains list`](#sf-domains-list) - [`sf domains nameservers DOMAIN`](#sf-domains-nameservers-domain) - [`sf domains nameservers set DOMAIN NAMESERVERS`](#sf-domains-nameservers-set-domain-nameservers) - [`sf domains registration DOMAIN`](#sf-domains-registration-domain) - [`sf domains registration purchase DOMAIN... [flags]`](#sf-domains-registration-purchase-domain-flags) - [`sf domains registration renew DOMAIN`](#sf-domains-registration-renew-domain) -- [`sf domains rm DOMAIN`](#sf-domains-rm-domain) +- [`sf domains remove DOMAIN`](#sf-domains-remove-domain) - [`sf domains search [QUERY]`](#sf-domains-search-query) - [`sf domains update DOMAIN`](#sf-domains-update-domain) - [`sf env`](#sf-env) -- [`sf env export-template [DIR]`](#sf-env-export-template-dir) - [`sf env import FILE`](#sf-env-import-file) -- [`sf env ls`](#sf-env-ls) +- [`sf env list`](#sf-env-list) - [`sf env pull [FILE]`](#sf-env-pull-file) -- [`sf env rm NAME`](#sf-env-rm-name) +- [`sf env remove NAME`](#sf-env-remove-name) - [`sf env set NAME [VALUE]`](#sf-env-set-name-value) +- [`sf env template [DIR]`](#sf-env-template-dir) - [`sf feedback`](#sf-feedback) -- [`sf feedback get REF`](#sf-feedback-get-ref) +- [`sf feedback inspect REF`](#sf-feedback-inspect-ref) - [`sf feedback list`](#sf-feedback-list) - [`sf fetch [PATH]`](#sf-fetch-path) - [`sf functions`](#sf-functions) @@ -175,7 +175,7 @@ GLOBAL FLAGS - [`sf git github`](#sf-git-github) - [`sf git github installations`](#sf-git-github-installations) - [`sf git github repos [INSTALLATION]`](#sf-git-github-repos-installation) -- [`sf git ls`](#sf-git-ls) +- [`sf git inspect`](#sf-git-inspect) - [`sf git origin`](#sf-git-origin) - [`sf git sync`](#sf-git-sync) - [`sf git update`](#sf-git-update) @@ -191,14 +191,21 @@ GLOBAL FLAGS - [`sf mcp install`](#sf-mcp-install) - [`sf mcp proxy`](#sf-mcp-proxy) - [`sf migrate URL`](#sf-migrate-url) +- [`sf oauth-apps`](#sf-oauth-apps) +- [`sf oauth-apps create`](#sf-oauth-apps-create) +- [`sf oauth-apps delete APP`](#sf-oauth-apps-delete-app) +- [`sf oauth-apps inspect APP`](#sf-oauth-apps-inspect-app) +- [`sf oauth-apps list`](#sf-oauth-apps-list) +- [`sf oauth-apps secret rotate APP`](#sf-oauth-apps-secret-rotate-app) +- [`sf oauth-apps update APP`](#sf-oauth-apps-update-app) - [`sf open [TARGET]`](#sf-open-target) -- [`sf operations [ID]`](#sf-operations-id) +- [`sf operations [OPERATION]`](#sf-operations-operation) - [`sf pages`](#sf-pages) - [`sf pages pull [TARGET]`](#sf-pages-pull-target) - [`sf pages validate`](#sf-pages-validate) - [`sf plugins`](#sf-plugins) - [`sf profiles`](#sf-profiles) -- [`sf profiles rm NAME`](#sf-profiles-rm-name) +- [`sf profiles remove NAME`](#sf-profiles-remove-name) - [`sf profiles set NAME`](#sf-profiles-set-name) - [`sf profiles use NAME`](#sf-profiles-use-name) - [`sf promote [VERSION]`](#sf-promote-version) @@ -210,10 +217,10 @@ GLOBAL FLAGS - [`sf routing inspect`](#sf-routing-inspect) - [`sf rules`](#sf-rules) - [`sf rules add`](#sf-rules-add) -- [`sf rules ls`](#sf-rules-ls) +- [`sf rules list`](#sf-rules-list) - [`sf rules pause`](#sf-rules-pause) +- [`sf rules remove NAME`](#sf-rules-remove-name) - [`sf rules resume`](#sf-rules-resume) -- [`sf rules rm NAME`](#sf-rules-rm-name) - [`sf rules test URL`](#sf-rules-test-url) - [`sf rules validate`](#sf-rules-validate) - [`sf runtime`](#sf-runtime) @@ -221,13 +228,13 @@ GLOBAL FLAGS - [`sf sell activate`](#sf-sell-activate) - [`sf sell demo`](#sf-sell-demo) - [`sf sell onboard`](#sf-sell-onboard) -- [`sf sell orders get SESSIONID`](#sf-sell-orders-get-sessionid) -- [`sf sell orders ls`](#sf-sell-orders-ls) -- [`sf sell orders resend SESSIONID`](#sf-sell-orders-resend-sessionid) -- [`sf sell orders ship SESSIONID`](#sf-sell-orders-ship-sessionid) +- [`sf sell orders inspect ORDER`](#sf-sell-orders-inspect-order) +- [`sf sell orders list`](#sf-sell-orders-list) +- [`sf sell orders resend ORDER`](#sf-sell-orders-resend-order) +- [`sf sell orders ship ORDER`](#sf-sell-orders-ship-order) - [`sf sell products archive KEY`](#sf-sell-products-archive-key) - [`sf sell products create`](#sf-sell-products-create) -- [`sf sell products ls`](#sf-sell-products-ls) +- [`sf sell products list`](#sf-sell-products-list) - [`sf sell products update KEY`](#sf-sell-products-update-key) - [`sf sell refresh`](#sf-sell-refresh) - [`sf sell status`](#sf-sell-status) @@ -236,42 +243,43 @@ GLOBAL FLAGS - [`sf share`](#sf-share) - [`sf share check`](#sf-share-check) - [`sf share grant`](#sf-share-grant) -- [`sf share grant edit ID`](#sf-share-grant-edit-id) +- [`sf share grant update GRANT`](#sf-share-grant-update-grant) - [`sf share identity`](#sf-share-identity) - [`sf share identity create`](#sf-share-identity-create) - [`sf share identity grant`](#sf-share-identity-grant) -- [`sf share identity ls`](#sf-share-identity-ls) +- [`sf share identity list`](#sf-share-identity-list) - [`sf share identity revoke CONNECTION`](#sf-share-identity-revoke-connection) - [`sf share identity update CONNECTION`](#sf-share-identity-update-connection) - [`sf share link`](#sf-share-link) -- [`sf share link copy ID`](#sf-share-link-copy-id) +- [`sf share link copy LINK`](#sf-share-link-copy-link) - [`sf share link create`](#sf-share-link-create) -- [`sf share link edit ID`](#sf-share-link-edit-id) -- [`sf share link ls`](#sf-share-link-ls) -- [`sf share link revoke ID`](#sf-share-link-revoke-id) +- [`sf share link list`](#sf-share-link-list) +- [`sf share link revoke LINK`](#sf-share-link-revoke-link) +- [`sf share link update LINK`](#sf-share-link-update-link) - [`sf share list`](#sf-share-list) - [`sf share password`](#sf-share-password) - [`sf share password create`](#sf-share-password-create) -- [`sf share password ls`](#sf-share-password-ls) -- [`sf share password revoke ID`](#sf-share-password-revoke-id) -- [`sf share password rotate ID`](#sf-share-password-rotate-id) +- [`sf share password list`](#sf-share-password-list) +- [`sf share password revoke CREDENTIAL`](#sf-share-password-revoke-credential) +- [`sf share password rotate CREDENTIAL`](#sf-share-password-rotate-credential) - [`sf share people`](#sf-share-people) -- [`sf share people edit PERSON`](#sf-share-people-edit-person) - [`sf share people invite EMAIL`](#sf-share-people-invite-email) -- [`sf share people ls`](#sf-share-people-ls) +- [`sf share people list`](#sf-share-people-list) - [`sf share people remove PERSON`](#sf-share-people-remove-person) - [`sf share people resend PERSON`](#sf-share-people-resend-person) +- [`sf share people update PERSON`](#sf-share-people-update-person) - [`sf share request`](#sf-share-request) -- [`sf share request approve ID`](#sf-share-request-approve-id) -- [`sf share request deny ID`](#sf-share-request-deny-id) -- [`sf share request ls`](#sf-share-request-ls) -- [`sf share revoke ID`](#sf-share-revoke-id) +- [`sf share request approve REQUEST`](#sf-share-request-approve-request) +- [`sf share request deny REQUEST`](#sf-share-request-deny-request) +- [`sf share request list`](#sf-share-request-list) +- [`sf share revoke GRANT`](#sf-share-revoke-grant) - [`sf share token`](#sf-share-token) - [`sf share token create`](#sf-share-token-create) -- [`sf share token ls`](#sf-share-token-ls) -- [`sf share token revoke ID`](#sf-share-token-revoke-id) -- [`sf share token rotate ID`](#sf-share-token-rotate-id) +- [`sf share token list`](#sf-share-token-list) +- [`sf share token revoke CREDENTIAL`](#sf-share-token-revoke-credential) +- [`sf share token rotate CREDENTIAL`](#sf-share-token-rotate-credential) - [`sf skills`](#sf-skills) +- [`sf skills install`](#sf-skills-install) - [`sf skills remove`](#sf-skills-remove) - [`sf skills status`](#sf-skills-status) - [`sf source archive`](#sf-source-archive) @@ -280,41 +288,41 @@ GLOBAL FLAGS - [`sf source cat PATH`](#sf-source-cat-path) - [`sf source commits apply`](#sf-source-commits-apply) - [`sf source commits create`](#sf-source-commits-create) -- [`sf source commits get SHA`](#sf-source-commits-get-sha) -- [`sf source commits ls`](#sf-source-commits-ls) +- [`sf source commits inspect SHA`](#sf-source-commits-inspect-sha) +- [`sf source commits list`](#sf-source-commits-list) - [`sf source diff`](#sf-source-diff) - [`sf source import`](#sf-source-import) -- [`sf source ls`](#sf-source-ls) +- [`sf source list`](#sf-source-list) - [`sf source merge SOURCE TARGET`](#sf-source-merge-source-target) - [`sf source search PATTERN`](#sf-source-search-pattern) - [`sf source tags create NAME`](#sf-source-tags-create-name) -- [`sf source tags ls`](#sf-source-tags-ls) +- [`sf source tags list`](#sf-source-tags-list) - [`sf spaces`](#sf-spaces) -- [`sf spaces add`](#sf-spaces-add) - [`sf spaces archive`](#sf-spaces-archive) - [`sf spaces check NAME`](#sf-spaces-check-name) - [`sf spaces claim`](#sf-spaces-claim) +- [`sf spaces create`](#sf-spaces-create) +- [`sf spaces delete`](#sf-spaces-delete) - [`sf spaces download`](#sf-spaces-download) - [`sf spaces duplicate`](#sf-spaces-duplicate) -- [`sf spaces get`](#sf-spaces-get) -- [`sf spaces ls`](#sf-spaces-ls) +- [`sf spaces inspect`](#sf-spaces-inspect) +- [`sf spaces key rotate`](#sf-spaces-key-rotate) +- [`sf spaces list`](#sf-spaces-list) - [`sf spaces restore`](#sf-spaces-restore) -- [`sf spaces rm`](#sf-spaces-rm) -- [`sf spaces rotate-claim`](#sf-spaces-rotate-claim) - [`sf spaces transfer TEAM`](#sf-spaces-transfer-team) - [`sf spaces update`](#sf-spaces-update) - [`sf status`](#sf-status) - [`sf storage [TARGET]`](#sf-storage-target) -- [`sf storage get ID`](#sf-storage-get-id) -- [`sf storage ls [TARGET]`](#sf-storage-ls-target) +- [`sf storage delete [OBJECT]`](#sf-storage-delete-object) +- [`sf storage download OBJECT`](#sf-storage-download-object) +- [`sf storage list [TARGET]`](#sf-storage-list-target) - [`sf storage put FILE`](#sf-storage-put-file) -- [`sf storage rm [ID]`](#sf-storage-rm-id) - [`sf switch [TEAM]`](#sf-switch-team) - [`sf tags`](#sf-tags) - [`sf tags apply FILE`](#sf-tags-apply-file) - [`sf tags debug`](#sf-tags-debug) - [`sf tags releases`](#sf-tags-releases) -- [`sf tags releases get ENVIRONMENT`](#sf-tags-releases-get-environment) +- [`sf tags releases inspect ENVIRONMENT`](#sf-tags-releases-inspect-environment) - [`sf tags releases rollback ENVIRONMENT REVISION`](#sf-tags-releases-rollback-environment-revision) - [`sf tags releases set ENVIRONMENT REVISION`](#sf-tags-releases-set-environment-revision) - [`sf tags revisions`](#sf-tags-revisions) @@ -328,56 +336,61 @@ GLOBAL FLAGS - [`sf teams`](#sf-teams) - [`sf teams accept INVITATION`](#sf-teams-accept-invitation) - [`sf teams create NAME`](#sf-teams-create-name) -- [`sf teams defaults [ROOTACCESS]`](#sf-teams-defaults-rootaccess) +- [`sf teams defaults [ACCESS]`](#sf-teams-defaults-access) - [`sf teams domains`](#sf-teams-domains) - [`sf teams domains add DOMAIN`](#sf-teams-domains-add-domain) -- [`sf teams domains ls`](#sf-teams-domains-ls) -- [`sf teams domains rm DOMAIN`](#sf-teams-domains-rm-domain) +- [`sf teams domains list`](#sf-teams-domains-list) +- [`sf teams domains remove DOMAIN`](#sf-teams-domains-remove-domain) - [`sf teams invitations`](#sf-teams-invitations) -- [`sf teams invitations add EMAILS`](#sf-teams-invitations-add-emails) - [`sf teams invitations cancel INVITATION`](#sf-teams-invitations-cancel-invitation) -- [`sf teams invitations ls`](#sf-teams-invitations-ls) +- [`sf teams invitations list`](#sf-teams-invitations-list) - [`sf teams invitations resend INVITATION`](#sf-teams-invitations-resend-invitation) - [`sf teams join [TEAM]`](#sf-teams-join-team) - [`sf teams join-link`](#sf-teams-join-link) -- [`sf teams ls`](#sf-teams-ls) +- [`sf teams list`](#sf-teams-list) - [`sf teams members`](#sf-teams-members) -- [`sf teams members ls`](#sf-teams-members-ls) -- [`sf teams members rm MEMBER`](#sf-teams-members-rm-member) +- [`sf teams members invite EMAILS`](#sf-teams-members-invite-emails) +- [`sf teams members list`](#sf-teams-members-list) +- [`sf teams members remove MEMBER`](#sf-teams-members-remove-member) - [`sf teams subteams`](#sf-teams-subteams) - [`sf teams subteams create NAME`](#sf-teams-subteams-create-name) -- [`sf teams subteams ls`](#sf-teams-subteams-ls) +- [`sf teams subteams delete SUBTEAM`](#sf-teams-subteams-delete-subteam) +- [`sf teams subteams list`](#sf-teams-subteams-list) - [`sf teams subteams members`](#sf-teams-subteams-members) - [`sf teams subteams members add SUBTEAM USER`](#sf-teams-subteams-members-add-subteam-user) -- [`sf teams subteams members ls SUBTEAM`](#sf-teams-subteams-members-ls-subteam) -- [`sf teams subteams members rm SUBTEAM USER`](#sf-teams-subteams-members-rm-subteam-user) +- [`sf teams subteams members list SUBTEAM`](#sf-teams-subteams-members-list-subteam) +- [`sf teams subteams members remove SUBTEAM USER`](#sf-teams-subteams-members-remove-subteam-user) - [`sf teams subteams rename SUBTEAM NAME`](#sf-teams-subteams-rename-subteam-name) -- [`sf teams subteams rm SUBTEAM`](#sf-teams-subteams-rm-subteam) - [`sf teams switch [TEAM]`](#sf-teams-switch-team) -- [`sf transfers accept ID`](#sf-transfers-accept-id) -- [`sf transfers cancel ID`](#sf-transfers-cancel-id) -- [`sf transfers get ID`](#sf-transfers-get-id) -- [`sf transfers ls`](#sf-transfers-ls) +- [`sf transfers accept TRANSFER`](#sf-transfers-accept-transfer) +- [`sf transfers cancel TRANSFER`](#sf-transfers-cancel-transfer) +- [`sf transfers inspect TRANSFER`](#sf-transfers-inspect-transfer) +- [`sf transfers list`](#sf-transfers-list) - [`sf unlink`](#sf-unlink) - [`sf users`](#sf-users) - [`sf users delete USER`](#sf-users-delete-user) -- [`sf users get USER`](#sf-users-get-user) -- [`sf users ls`](#sf-users-ls) +- [`sf users inspect USER`](#sf-users-inspect-user) +- [`sf users list`](#sf-users-list) - [`sf users reactivate USER`](#sf-users-reactivate-user) - [`sf users revoke USER`](#sf-users-revoke-user) - [`sf users sessions USER`](#sf-users-sessions-user) - [`sf users settings`](#sf-users-settings) - [`sf users suspend USER`](#sf-users-suspend-user) - [`sf versions`](#sf-versions) -- [`sf versions get [VERSION]`](#sf-versions-get-version) -- [`sf versions ls`](#sf-versions-ls) -- [`sf versions rm [VERSION]`](#sf-versions-rm-version) +- [`sf versions delete [VERSION]`](#sf-versions-delete-version) +- [`sf versions inspect [VERSION]`](#sf-versions-inspect-version) +- [`sf versions list`](#sf-versions-list) - [`sf whoami`](#sf-whoami) - [`sf wp`](#sf-wp) - [`sf zero`](#sf-zero) - [`sf zero abilities`](#sf-zero-abilities) +- [`sf zero build`](#sf-zero-build) - [`sf zero call ABILITY`](#sf-zero-call-ability) -- [`sf zero import SOURCE DIRECTORY`](#sf-zero-import-source-directory) +- [`sf zero check`](#sf-zero-check) +- [`sf zero dev`](#sf-zero-dev) +- [`sf zero import SOURCE INPUT`](#sf-zero-import-source-input) +- [`sf zero init [NAME]`](#sf-zero-init-name) +- [`sf zero migrate`](#sf-zero-migrate) - [`sf zero queries`](#sf-zero-queries) - [`sf zero types`](#sf-zero-types) @@ -387,39 +400,43 @@ Revoke visitor sessions. ```text USAGE - $ sf access [--profile ] [-y] + $ sf access [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] [--profile + ] [--token ] [-y] GLOBAL FLAGS + -d, --debug Print redacted diagnostic detail on stderr (SPACEFAST_DEBUG). -y, --yes [env: SPACEFAST_YES] Skip confirmation prompts. - --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. - --json Format output as json. - --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. + --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. + --cwd= Working directory for this invocation. + --[no-]json Print the result as JSON. + --no-color Disable color (NO_COLOR). + --non-interactive Never prompt; fail with the required inputs instead (SPACEFAST_NON_INTERACTIVE). + --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. DESCRIPTION Revoke visitor sessions. Revoke active visitor sessions. Use `sf share` to manage who can open a space. - -EXAMPLES - Revoke visitor sessions. - - $ sf access ``` -## `sf access logout-all` +## `sf access sessions revoke` Revoke all visitor sessions. ```text USAGE - $ sf access logout-all [--profile ] [-y] [--claim-token - ] [-o ] [--space ] + $ sf access sessions revoke [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] [--profile + ] [--token ] [-y] [-T ] [--space ] GLOBAL FLAGS + -d, --debug Print redacted diagnostic detail on stderr (SPACEFAST_DEBUG). -y, --yes [env: SPACEFAST_YES] Skip confirmation prompts. - --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. - --json Format output as json. - --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. + --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. + --cwd= Working directory for this invocation. + --[no-]json Print the result as JSON. + --no-color Disable color (NO_COLOR). + --non-interactive Never prompt; fail with the required inputs instead (SPACEFAST_NON_INTERACTIVE). + --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. DESCRIPTION Revoke all visitor sessions. @@ -429,7 +446,7 @@ DESCRIPTION EXAMPLES Revoke every visitor session on the space. - $ sf access logout-all --space docs + $ sf access sessions revoke --space docs ``` ## `sf activity` @@ -438,23 +455,27 @@ Show activity events. ```text USAGE - $ sf activity [--profile ] [-y] [--claim-token - ] [-o ] [--space ] [--all] [--code ] [--since ] [--until ] [--limit - ] [--cursor ] + $ sf activity [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] [--profile + ] [--token ] [-y] [-T ] [--space ] [--all] [--code ] + [--since ] [--until ] [--limit ] [--cursor ] FLAGS - --all Read account-wide activity instead of a single space. - --code= Filter by activity taxonomy code, for example version.promoted. - --cursor= Pagination cursor from a previous response. - --limit= Maximum number of events to return (default 50, max 100). - --since= Only events at or after this ISO timestamp. - --until= Only events before this ISO timestamp. + --all Read account-wide activity instead of a single space. + --code= Filter by activity taxonomy code, for example version.promoted. + --cursor= Pagination cursor from a previous response. + --limit= Maximum number of events to return (default 50, max 100). + --since= Only events at or after this ISO timestamp. + --until= Only events before this ISO timestamp. GLOBAL FLAGS + -d, --debug Print redacted diagnostic detail on stderr (SPACEFAST_DEBUG). -y, --yes [env: SPACEFAST_YES] Skip confirmation prompts. - --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. - --json Format output as json. - --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. + --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. + --cwd= Working directory for this invocation. + --[no-]json Print the result as JSON. + --no-color Disable color (NO_COLOR). + --non-interactive Never prompt; fail with the required inputs instead (SPACEFAST_NON_INTERACTIVE). + --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. DESCRIPTION Show activity events. @@ -475,8 +496,8 @@ Write Spacefast agent instructions. ```text USAGE - $ sf agents init [DIRECTORY] [--profile ] [-y] [--agent - generic|claude-code] + $ sf agents init [DIRECTORY] [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] + [--profile ] [--token ] [-y] [--agent generic|claude-code] ARGUMENTS [DIRECTORY] Directory that will contain AGENTS.md and CLAUDE.md. Defaults to the current directory. @@ -486,10 +507,14 @@ FLAGS GLOBAL FLAGS + -d, --debug Print redacted diagnostic detail on stderr (SPACEFAST_DEBUG). -y, --yes [env: SPACEFAST_YES] Skip confirmation prompts. - --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. - --json Format output as json. - --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. + --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. + --cwd= Working directory for this invocation. + --[no-]json Print the result as JSON. + --no-color Disable color (NO_COLOR). + --non-interactive Never prompt; fail with the required inputs instead (SPACEFAST_NON_INTERACTIVE). + --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. DESCRIPTION Write Spacefast agent instructions. @@ -512,18 +537,22 @@ Print runtime analytics. ```text USAGE - $ sf analytics [--profile ] [-y] [--claim-token - ] [-o ] [--space ] [--window 48h|7d|30d] + $ sf analytics [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] [--profile + ] [--token ] [-y] [-T ] [--space ] [--window 48h|7d|30d] FLAGS --window=