@anyday/sanity-workspace-manager
The command-line tool for Anyday's Sanity workspace manager. The workspace manager creates a new
Sanity workspace for a site in a few clicks: the project, its dataset, starter or copied content,
the brand, the central Studio entry and the Vercel site. It holds no code for any one project.
Each kind of site (a project type) is described instead by a workspace-manager.json manifest
that lives in that site's repo.
This tool makes and saves that manifest, the way sanity typegen generate makes types:
generatewritesworkspace-manager.jsonfrom the repo's schema, the live Sanity project andworkspace-manager.config.json, which holds your answers. Change the schema, run it again.checkfails when the manifest is out of date with the schema or was edited by hand.registersaves the manifest to the workspace manager, which then offers the project type in the Workspaces app.
Requirements
- Node.js 20 or later.
- A Sanity login for
generateandregister: runnpx sanity loginonce, or setSANITY_AUTH_TOKEN.checkneeds none. - For
register, the Administrator role in the manifest's Sanity organisation.
Quick start
From the root of the site repo:
# 1. Export the Studio's schema (writes sanity.schema.json)
cd studio && npx sanity schema extract && cd ..
# 2. Answer a few questions and pick starter pages; writes the config and the manifest
npx @anyday/sanity-workspace-manager generate
# 3. Review workspace-manager.config.json, commit both files next to the schema
# 4. Save it to the workspace manager
npx @anyday/sanity-workspace-manager register --api https://anyday-workspace-manager-sanity-api.vercel.app
After a schema change: extract the schema, run generate, commit, register.
The two files
| File | Holds | Edited by |
|---|---|---|
workspace-manager.config.json | Your answers: ids, copy sections, colour role descriptions, starter pages, ... | You |
workspace-manager.json | Everything else: field mappings, theme defaults, starter page content | generate |
Never edit workspace-manager.json: its $generated line holds a hash, and check fails after a
hand edit. Change the config and run generate instead. The config has a JSON Schema, so your
editor completes and checks it once the package is installed.
Add this line to the site repo's .gitattributes, so GitHub marks the manifest as generated and
collapses its diff in PRs; generate reminds you until it's there:
workspace-manager.json linguist-generated=true
| Config field | Holds |
|---|---|
id | Permanent: jobs store it and it prefixes each site's Vercel project |
title | The name shown in the Workspaces app |
organizationId | The Sanity organisation new workspaces are created in |
vercelTeamId | The client's own Vercel team; left out for Anyday's |
site | The GitHub repo and branch each new site deploys from |
studioOrigins | The Studios that open new workspaces, incl. a site's /studio; picked from the live project on first run |
sections | Each content type with its label in the Copy picker, or false to leave it out |
colorRoles | Each theme colour with a short description of where it's used, shown to whoever sets up a brand |
themeFiles | Theme fields with uploaded files (fonts) to copy into new sites; fonts are often licensed per brand |
starter | What Start fresh writes: picked live pages, or [] for one empty home page (see below) |
shared | For a client whose sites share one project: its projectId, siteIdField, slugPrefix, and the sampleSiteId whose theme and pages generate reads |
siteEnv | Env var names the site reads when .env.example doesn't name them, siteId for a shared type, fixed templated values, and linkShared team-shared variables to link |
site.rootDirectory, site.vercelProjectName | The site's folder in the repo, and a template for its Vercel project name (e.g. {{siteId}}) |
| ... | singletons, homePage, settingsFields: guessed from the schema on the first run, asked when they break |
generate
npx @anyday/sanity-workspace-manager generate [dir] [options]
dir is the site repo, and defaults to the current folder. The first run asks who the project type
is for, each with a suggestion you accept with Enter, then lists the live site's pages to pick
starter pages from. Answers go in the config. Later runs reuse them and ask only about what changed
in the schema:
generalSettings.logo is gone. Which field is the logo? [brandLogo/favicon/none] (none):
Copy section mediaMention is gone from the schema. Was it renamed? [pressMention/no] (no):
New content type event: offer it as a copy section? [y/n] (y):
A renamed field reads as one removed and one added, so generate asks rather than guesses.
| Option | Does |
|---|---|
--yes | Takes the suggested answers without asking, and lists them |
--offline | Doesn't read the live project: keeps the live values already in the manifest |
--diff | Shows what would change in both files, and writes nothing |
--ask-again | Asks the saved questions again, e.g. to change copy sections or pick starter pages |
--project <id> | The Sanity project new workspaces copy their schema from |
--org <id> | The Sanity organisation new workspaces are created in |
--vercel-team <id> | The client's own Vercel team for the sites |
--studio <origin> | The Studio that opens new workspaces, e.g. https://acme.sanity.studio |
--dataset <name> | The dataset live values are read from (default production) |
--starter <pages> | Starter pages without the list: home, slugs or ids, comma-separated |
What it reads
| Source | Used for |
|---|---|
| git remote and default branch | site, the suggested id and name |
.env.example, names only | siteEnv, the variables each new site needs (*_WORKSPACES_API_URL if the site has one) |
sanity.schema.json | copy sections, singletons, home page, language documents, settings fields, colours and fonts |
| the live project | organisation, Studios and CORS origins to pick from, theme colours and defaults, fonts named in themeFiles, starter pages |
sanity.schema.json is looked for in the repo root, frontend, web, site, studio, apps
and each folder in apps.
Without access to the live project
generate says first why it can't read the live project, and what to do:
| Situation | What it prints | Fix |
|---|---|---|
| Not signed in | Not signed in to Sanity: run npx sanity login to read live values | Run npx sanity login |
| Expired or bad session | Your Sanity session is missing or expired: run npx sanity login … | Run npx sanity login; if SANITY_AUTH_TOKEN is set, it wins over the login, so fix or unset it |
| No access | Your Sanity account can't read project <id>: ask one of its Administrators … | Ask an Administrator of that project to add you (Viewer is enough) |
| Anything else | Couldn't read the live project <id>: <reason> | Follow the reason, e.g. a wrong --project or --dataset |
It then offers to continue from the schema only. Values only the live project holds (colours,
fonts, starter pages) are kept from the existing manifest, and the last line says so. On a first
run they start as placeholders: black colours, system fonts and one empty home page, and
register asks before saving them. Sign in and run generate again to read the live values.
Starter pages
Starter pages are what a new workspace starts with when it doesn't copy another workspace. They always come from the live site, so you update them in the Studio, never by hand in a file. The first run lists the live pages in the home page's language, home first:
Pages of the live site:
1. Home (/home) ← home
2. About us (/about-us)
3. Contact (/contact)
Starter pages, by number (e.g. 1,3; Enter for one empty home page):
The config saves only your pick, e.g. "starter": ["home", "about-us"], and every run reads those
pages again. home means whichever page the settings mark as the home page; anything else is a
slug or document id. [] means one empty home page.
To change the starter pages later, run generate --ask-again and pick again from the list, or
edit the config's starter and run generate.
A new workspace has only its starter pages, so generate removes each link to another document (a
doctor card, a button to another page) and lists what it removed. Fields the schema no longer has
are dropped too. Starter pages get {{siteName}} and {{language}} where the source site's name
and language were.
Moving from a hand-made manifest
Run generate in a repo that has a workspace-manager.json and no config: it moves every choice
the manifest holds, including copy section labels, colour descriptions and fonts, into the config.
It then lists the live pages to pick starter pages from; until you pick, it keeps the manifest's
existing starter pages.
check
npx @anyday/sanity-workspace-manager check [dir]
Read-only. Fails, naming each difference, when:
- the schema changed in a way the manifest uses (a renamed or removed field, a new content type),
workspace-manager.jsonwas edited by hand, or- the starter pages have fields the schema no longer has.
Only changes to fields the manifest uses matter: the logo, site name, theme colours and fonts,
home page, language fields, content types, and the fields on starter pages. check reads only the
repo, so it can run in CI without signing in.
register
npx @anyday/sanity-workspace-manager register [dir] --api <url>
Checks workspace-manager.json in dir (default: the current folder) and saves it to the
workspace manager. The live API is https://anyday-workspace-manager-sanity-api.vercel.app; set
WORKSPACE_MANAGER_API_URL to skip --api.
- You must be an Administrator of the manifest's
organizationId. - Registering again replaces the saved copy for that
id, so a changed manifest needs no delete first. - It asks before saving placeholder colours or fonts;
--yesskips the question. --dry-runchecks the manifest without saving it.- It warns when the manifest names a Vercel team the API has no token for yet. A client's own team
needs its token on the API as
VERCEL_TOKEN_<team id>(a secret, added on Vercel by Anyday).
Signing in
All commands use SANITY_AUTH_TOKEN when it is set, and otherwise the token npx sanity login
saved. Use your own login: the workspace manager refuses robot tokens for register, so that
every project type traces back to a person. If a robot token is exported in your shell (for
example for a Studio deploy), unset SANITY_AUTH_TOKEN before registering.
Troubleshooting
| Message | Fix |
|---|---|
No sanity.schema.json | Run npx sanity schema extract in the Studio folder |
workspace-manager.json is out of date | Run generate and commit both files |
was edited by hand | Put the change in the config, then run generate |
No page with the slug or id "…" | Fix the config's starter, or run generate --ask-again to pick again |
No GitHub remote found | Add an origin remote, or set site.repo in the config |
Not signed in to Sanity | Run npx sanity login |
Your Sanity session is missing or expired | Run npx sanity login, or fix or unset SANITY_AUTH_TOKEN |
Your Sanity account can't read project … | Ask an Administrator of that project to add you (Viewer is enough) |
Couldn't read the live project … | Follow the reason, e.g. a wrong --project or --dataset |
Pass --api <url> or set WORKSPACE_MANAGER_API_URL | Add --api https://anyday-workspace-manager-sanity-api.vercel.app |
Use your own Sanity login, not a robot token | unset SANITY_AUTH_TOKEN and use npx sanity login |
Register only as an Administrator of organisation … | Ask an Administrator of that organisation to run register |
How it fits together
The full design, including every manifest field, is in docs/project-config.md, and how generate works in docs/manifest-generate.md (Anyday's GitHub organisation only).
Developing and publishing
From a clone of the workspace manager repo, run the CLI without npm:
npm run cli -- generate ../my-site. npm run build bundles src/cli.ts, with the manifest code
it borrows from the API, into dist/cli.mjs; only @sanity/client and ajv stay dependencies.
Publish from main: bump version, then run
npm publish --workspace=@anyday/sanity-workspace-manager --access public --auth-type=web from the
repo root. It builds and tests first; the web sign-in covers npm's two-factor check, and the
release then waits for approval on npmjs.com.