@scribe-platform/cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/commands/login.d.ts.map +1 -1
  2. package/dist/commands/login.js +15 -1
  3. package/dist/commands/login.js.map +1 -1
  4. package/dist/commands/prospect.d.ts +27 -0
  5. package/dist/commands/prospect.d.ts.map +1 -0
  6. package/dist/commands/prospect.js +213 -0
  7. package/dist/commands/prospect.js.map +1 -0
  8. package/dist/commands/request.d.ts +38 -0
  9. package/dist/commands/request.d.ts.map +1 -0
  10. package/dist/commands/request.js +214 -0
  11. package/dist/commands/request.js.map +1 -0
  12. package/dist/commands/site.d.ts +11 -0
  13. package/dist/commands/site.d.ts.map +1 -1
  14. package/dist/commands/site.js +41 -3
  15. package/dist/commands/site.js.map +1 -1
  16. package/dist/commands/update.d.ts +10 -0
  17. package/dist/commands/update.d.ts.map +1 -1
  18. package/dist/commands/update.js +128 -31
  19. package/dist/commands/update.js.map +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/claude-hook.d.ts +74 -0
  24. package/dist/lib/claude-hook.d.ts.map +1 -0
  25. package/dist/lib/claude-hook.js +108 -0
  26. package/dist/lib/claude-hook.js.map +1 -0
  27. package/dist/lib/client.d.ts +182 -0
  28. package/dist/lib/client.d.ts.map +1 -1
  29. package/dist/lib/client.js +82 -0
  30. package/dist/lib/client.js.map +1 -1
  31. package/dist/lib/skill.d.ts +13 -0
  32. package/dist/lib/skill.d.ts.map +1 -1
  33. package/dist/lib/skill.js +33 -2
  34. package/dist/lib/skill.js.map +1 -1
  35. package/dist/lib/update-check.d.ts +46 -0
  36. package/dist/lib/update-check.d.ts.map +1 -0
  37. package/dist/lib/update-check.js +103 -0
  38. package/dist/lib/update-check.js.map +1 -0
  39. package/package.json +1 -1
  40. package/skills/scribe-site-builder/SKILL.md +155 -33
  41. package/skills/scribe-site-builder/recipes/README.md +10 -3
  42. package/skills/scribe-site-builder/recipes/domains.md +151 -0
  43. package/skills/scribe-site-builder/recipes/forms.md +5 -2
  44. package/skills/scribe-site-builder/recipes/prospects.md +192 -0
  45. package/skills/scribe-site-builder/recipes/requests.md +145 -0
  46. package/skills/scribe-site-builder/recipes/schema.md +24 -0
@@ -1,14 +1,19 @@
1
1
  ---
2
2
  name: scribe-site-builder
3
- description: Guides building a website on Scribe CMS end to end — designing the Astro site freely against Scribe as its backend, editing the `.scribe.yml` schema, adding one or many forms, the local dev loop, pushing content, and deploying — using the `scribe` CLI and the recipes in `recipes/`. Also the reference for working an existing Scribe site (adding fields/collections, publishing content, triaging form submissions, archiving, restoring or deleting a site). Use whenever the user wants to build/create/start a new website or site with Scribe, add a form to a site, work on an existing Scribe site, or asks about the `scribe` CLI.
3
+ description: Guides building a website on Scribe CMS end to end — designing the Astro site freely against Scribe as its backend, editing the `.scribe.yml` schema, adding one or many forms, the local dev loop, pushing content, and deploying — using the `scribe` CLI and the recipes in `recipes/`. Also the reference for working an existing Scribe site (adding fields/collections, publishing content, triaging form submissions and inquiries, renaming, archiving, restoring or deleting a site), for taking a site live on a custom domain — checking whether a client has a domain, buying one, DNS/CNAME setup, GitHub Pages domain config, HTTPS — for the queue of requests clients have sent the agency (triaging, claiming, replying, closing), and for the agency's prospect pipeline (logging calls, follow-ups, won/lost). Use whenever the user wants to build/create/start a new website or site with Scribe, add a form to a site, work on an existing Scribe site, publish/launch/go live with a site, point a site at a domain, set up or fix a custom domain or DNS for a Scribe site, work the inquiries a site has received, triage or reply to a client's request for work, log a call or follow-up with a prospect or client, or asks about the `scribe` CLI.
4
4
  ---
5
5
 
6
6
  # Building a site with Scribe
7
7
 
8
- Source of truth: this file is the merged, agent-facing version of
9
- `docs/BUILDER-GUIDE.md` and `docs/AGENT-GUIDE.md` in the
10
- `ai-xerro-agent/projects/scribe` repo. If those docs and this one ever
11
- disagree, the repo docs win — this file may be behind.
8
+ **This file and the `recipes/` beside it are the whole reference.** They ship
9
+ inside the `@scribe-platform/cli` npm package and are installed with it, so
10
+ everything needed to drive Scribe is here or in `scribe <command> --help`.
11
+ There is no repo to consult and no wiki to look up — if something seems to be
12
+ missing, it is a gap worth telling Dave about, not a file to go hunting for.
13
+
14
+ (It is generated from `docs/BUILDER-GUIDE.md` and `docs/AGENT-GUIDE.md` in the
15
+ Scribe repo. That matters only to whoever edits it there; those paths do not
16
+ exist on a machine that installed the CLI from npm.)
12
17
 
13
18
  ## Recipes — read the one you need
14
19
 
@@ -22,6 +27,9 @@ instead of reading all of them:
22
27
  | [`recipes/collections.md`](./recipes/collections.md) | rendering editor-managed content |
23
28
  | [`recipes/forms.md`](./recipes/forms.md) | adding one or more forms |
24
29
  | [`recipes/deploy.md`](./recipes/deploy.md) | build/deploy setup or debugging |
30
+ | [`recipes/domains.md`](./recipes/domains.md) | anything about a domain — whether the client has one, getting them one, the DNS, taking a site live on it, fixing one |
31
+ | [`recipes/requests.md`](./recipes/requests.md) | working the queue of jobs clients have asked the agency for |
32
+ | [`recipes/prospects.md`](./recipes/prospects.md) | logging calls and follow-ups on prospects (not site-building) |
25
33
 
26
34
  [`recipes/README.md`](./recipes/README.md) lists the five things that are a hard
27
35
  contract (because Scribe writes into the repo) as opposed to advice.
@@ -32,6 +40,16 @@ contract (because Scribe writes into the repo) as opposed to advice.
32
40
  repo. The admin app and the `scribe` CLI are both just clients that read/write
33
41
  files in it via the GitHub API; a local `git clone` is a third client reading/writing
34
42
  the same files via git.
43
+ - **Scribe owns the repo's GitHub Pages settings — never set them by hand.** The
44
+ custom domain, `public/CNAME`, HTTPS enforcement and the build's base path are
45
+ all written by `scribe site promote`, in one idempotent run. Do **not** reach for
46
+ `gh api .../pages`, the GitHub web UI, or a hand-written `public/CNAME`: setting
47
+ one without the others leaves the site 404ing, and the Pages call needs repo
48
+ admin that a site user doesn't have. The CLI doesn't need it — the admin app
49
+ makes that call with Scribe's own org token, so a write-scoped API key is enough.
50
+ If something about a domain looks like it needs a GitHub permission you lack,
51
+ that's the signal you're on the wrong path, not that you need Dave.
52
+ See [`recipes/domains.md`](./recipes/domains.md).
35
53
  - **Form submissions are the exception — they live in MongoDB, not the repo** (RFC
36
54
  0004). A Visitor's name, email and message used to be committed as a YAML file into
37
55
  the site's public repo, which published them to the open internet. Nothing writes
@@ -60,41 +78,61 @@ contract (because Scribe writes into the repo) as opposed to advice.
60
78
 
61
79
  ## Setup (once)
62
80
 
63
- - **CLI**: confirm `scribe --help` works. If it's not found, `npm link` from
64
- `packages/cli` in the `ai-xerro-agent/projects/scribe` repo.
81
+ - **CLI**: confirm `scribe --help` works. If it's not found:
82
+ `npm install -g @scribe-platform/cli`. (Working inside a clone of the Scribe
83
+ repo itself? `npm link` from `packages/cli` instead — but that is a
84
+ maintainer's setup, not a user's.)
65
85
  - **Login**: `scribe login --token <key>` — mint the key from the
66
- admin UI (sign in → account menu → **API Tokens** → New token). Defaults to the
67
- hosted admin at `https://scribe.weaverdigital.io`; add `--api-url <url>` only to
68
- point at a different instance (a local dev/Docker admin). Writes
69
- `~/.scribe/config.json`; every other command reads from there, no per-invocation
70
- `--token` flag. If a command fails with a raw `fetch failed`, that's not an API
86
+ admin UI (sign in → account menu → **API Tokens** → New token). This is the
87
+ whole machine setup: it writes `~/.scribe/config.json`, installs this skill,
88
+ and turns on auto-updates. Defaults to the hosted admin at
89
+ `https://scribe.weaverdigital.io`; add `--api-url <url>` only to point at a
90
+ different instance (a local dev/Docker admin). Every other command reads that
91
+ config, no per-invocation `--token` flag. If a command fails with a raw `fetch failed`, that's not an API
71
92
  error — it means `apiUrl` in that config doesn't point at anything listening;
72
93
  re-run `scribe login` with the right URL.
73
94
 
74
95
  ## Keeping Scribe up to date
75
96
 
76
- `scribe update` updates the CLI **and** reinstalls this skill, which ships inside
77
- the `@scribe-platform/cli` npm package:
97
+ **This normally happens by itself and needs no attention.** `scribe login`
98
+ installs this skill and registers a Claude Code `SessionStart` hook that checks npm for
99
+ a newer `@scribe-platform/cli` at the start of **every** session and installs
100
+ it — CLI and this skill together — *before* the session's skills are scanned.
101
+ So a session either started on the current version or was brought to it before
102
+ anyone typed. That is the whole reason the check runs at session start rather
103
+ than mid-conversation: there is never a new skill sitting on disk that the
104
+ running session can't see.
105
+
106
+ If an update did install, the hook says so in this session's context. Don't
107
+ relay it; it's already done.
108
+
109
+ If the hook reports that an update **failed** to install, that is for Dave, not
110
+ for the user — it will be an npm permissions or registry problem. Say so plainly
111
+ and carry on; the old version still works.
112
+
113
+ `scribe update` also runs the whole thing by hand:
78
114
 
79
115
  ```
80
- scribe update
116
+ scribe update # update the CLI + this skill now
117
+ scribe update --check # just report whether a newer version exists
118
+ scribe update --install-hook # repair the auto-update hook if it went missing
81
119
  ```
82
120
 
83
- Two things to know:
121
+ One caveat: this **only works for a global npm install** (`npm install -g
122
+ @scribe-platform/cli`). In a dev checkout of the scribe repo it refuses, and if
123
+ `~/.claude/skills/scribe-site-builder` is a symlink into a checkout it leaves it
124
+ alone — that's an author editing the skill in place.
84
125
 
85
- - **Tell the user to restart Claude Code afterwards.** Claude Code only discovers
86
- skills at session start, so the session that ran the update is still reading the
87
- old copy of this file. If you need the new instructions in the current session,
88
- read `~/.claude/skills/scribe-site-builder/SKILL.md` from disk instead of waiting
89
- for a restart.
90
- - **It only works for a global npm install** (`npm install -g
91
- @scribe-platform/cli`). In a dev checkout of the scribe repo it refuses, and if
92
- `~/.claude/skills/scribe-site-builder` is a symlink into a checkout it leaves it
93
- alone — that's an author editing the skill in place.
126
+ Run it by hand when the user says there's a new version, or when a CLI command
127
+ fails in a way that suggests this skill is behind the API (an option the CLI
128
+ doesn't recognise, a command that no longer exists) — the session-start check
129
+ runs before the session, so anything published since then is not here yet.
94
130
 
95
- Run it when the user says there's a new version, or when a CLI command fails in a
96
- way that suggests this skill is behind the API (an option the CLI doesn't
97
- recognise, a command that no longer exists).
131
+ **If a hand-run update installs a new version mid-session**, tell the user in one
132
+ sentence, in their words: *"I've updated Scribe — close this chat and start a new
133
+ one so I pick up the new instructions."* Then stop relying on anything in this
134
+ file that the update might have changed; if you need the new text now, read
135
+ `~/.claude/skills/scribe-site-builder/SKILL.md` from disk.
98
136
 
99
137
  ## Workflow: building a new site
100
138
 
@@ -168,6 +206,11 @@ recognise, a command that no longer exists).
168
206
  8. **Domain cutover — only when a customer's own hostname is ready.** Skip this
169
207
  entirely for demo sites; the GitHub Pages default already works.
170
208
 
209
+ If it *isn't* ready — the client may not own a domain, may not know who holds
210
+ it, or may have mail on it — that part is the bulk of the work and is in
211
+ [`recipes/domains.md`](./recipes/domains.md) under "Before the cutover". The
212
+ client buys their own domain, at their own registrar, in their own name.
213
+
171
214
  Check what DNS the domain needs first. This changes nothing:
172
215
  ```
173
216
  scribe site promote my-site --domain example.com --dns-only
@@ -201,12 +244,22 @@ recognise, a command that no longer exists).
201
244
  sync — fix it in the Cloudflare dashboard, or the contact form silently rejects
202
245
  real visitors on that domain.
203
246
 
247
+ **You do not need GitHub repo admin rights for any of this.** The Pages API call
248
+ is made by the admin app using Scribe's org token, so a write-scoped API key is
249
+ enough. If you find yourself checking whether the user can administer the repo,
250
+ you've left the supported path.
251
+
204
252
  `scribe site update my-site --domain example.com` only edits the stored record
205
- and the Turnstile widget. Use it to correct a domain, never to cut a site over.
253
+ and the Turnstile widget. Use it to correct a domain, never to cut a site over —
254
+ the CLI now refuses that and points at `promote`. Deeper reference, including how
255
+ to repair a half-finished cutover: [`recipes/domains.md`](./recipes/domains.md).
206
256
 
207
257
  ## Working an existing site
208
258
 
209
259
  ```
260
+ scribe site promote <site> --domain <domain> [--dns-only] # take it live on a real domain
261
+ scribe site update <site> --turnstile-key <key> # correct stored settings only
262
+
210
263
  scribe content list <site> [--collection <name>] [--json]
211
264
  scribe content read <site> <path> [--json]
212
265
  scribe content publish <site> <file> # validates against schema
@@ -223,8 +276,19 @@ scribe submission list <site> <collection> [--status <status>] [--page <n>] [--j
223
276
  scribe submission update <site> <collection> <id> [--status <s>] [--note <n>]
224
277
  scribe submission export <site> <collection> [--format csv|json] [--status <s>] [--out <file>]
225
278
  scribe submission delete <site> <collection> <id> [--yes] # staff only
279
+
280
+ scribe request list [--site <slug>] [--status <s>] [--type <t>] [--mine|--unassigned] [--all] [--page <n>]
281
+ scribe request show <id> # the ask + the whole thread, internal notes included
282
+ scribe request claim <id> [--release]
283
+ scribe request reply <id> --message <text> [--internal]
284
+ scribe request status <id> <open|in-progress|waiting-on-you|done|cancelled>
226
285
  ```
227
286
 
287
+ - **"Publish/launch the site at example.com" means `scribe site promote`** — the full
288
+ domain cutover in one command — step 8 of the build workflow above, and in full in
289
+ [`recipes/domains.md`](./recipes/domains.md). It is not `content publish`, and
290
+ it is not `site update --domain` (which only rewrites the stored record and would
291
+ leave the site 404ing on the new host — the CLI refuses that now and tells you so).
228
292
  - `scribe schema apply` is the way to add a form (or any collection with fields): it
229
293
  takes a YAML/JSON file describing **one whole collection**, upserts it by `name`,
230
294
  and validates through the admin's own parser first — so a reserved `status` field,
@@ -254,6 +318,53 @@ scribe submission delete <site> <collection> <id> [--yes] # staff only
254
318
  **staff** role — a site user's API key gets a 403. It prompts for the id unless
255
319
  `--yes` is passed. There is deliberately no equivalent in the admin UI, so don't go
256
320
  looking for a delete button on the inbox.
321
+ - **`scribe request` is the other queue, and the opposite direction.** A submission is
322
+ a Visitor contacting the *client*; a Request is the **client contacting the agency**
323
+ to ask for work. One cross-site queue, staff-only, nothing in any repo. Three things
324
+ to know before touching it, with the rest in
325
+ [`recipes/requests.md`](./recipes/requests.md):
326
+ - **`waiting-on-you` means waiting on the _client_** — every status is phrased for
327
+ the Client's own dashboard. Read as an instruction to the agency it means the
328
+ opposite of what it says.
329
+ - **A visible `reply` is an email to the client**, sent immediately and permanently.
330
+ Show the user the text before sending unless they've said not to bother.
331
+ `--internal` is the Agency-only note: no email, invisible to the client, moves no
332
+ status.
333
+ - **There is no `create`.** A Request comes from the Client; filing one for them
334
+ would put words in their mouth. Say so rather than hunting for a flag.
335
+
336
+ ## A site's two names, and the jobs only the admin UI can do
337
+
338
+ A site has a **slug** and a **display name**, and only one of them moves.
339
+
340
+ - The **slug** (`riverside-dental`) is the site's identity: the repo name, the
341
+ Pages URL, `?site=` on the submissions endpoint, and the argument to every
342
+ `scribe` command. **There is no rename** — not in the CLI, not in the admin.
343
+ Moving a site to a different slug means provisioning a new site. So when
344
+ someone is choosing a slug, it is worth one sentence of care up front.
345
+ - The **display name** is a label, and is editable any time: in the Staff console,
346
+ **Sites → the site → the Name field** at the top of the Details card, saved on
347
+ blur. It is allowed on archived sites too. `scribe site add --name` sets it at
348
+ creation; nothing in the CLI changes it afterwards.
349
+
350
+ A rename reaches the consoles and notification subjects and **nothing a Visitor
351
+ sees** — not the URL, and not `.scribe.yml`'s `siteName`, which is the website's
352
+ own copy of its name in a repo the rename doesn't write to. If the name is wrong
353
+ on the published pages too, that's a separate content edit in the repo.
354
+
355
+ Three other things live only in the Staff console, so route the user there rather
356
+ than hunting for a flag:
357
+
358
+ | Job | Where |
359
+ |---|---|
360
+ | Rename (display name) | Sites → the site → Name |
361
+ | Attach a site to a Client, or detach it | Sites → the site → **Client** |
362
+ | Star a site (sorts it to the top of the Sites list, and is what the staff Home page's Sites card shows) | Sites → the star |
363
+
364
+ And one that isn't available at all: **creating a login for a client's own
365
+ people**. There is no `scribe` command for it and no self-serve invite — user
366
+ management is super-admin only. "Give the client access to edit their content" is
367
+ a request to hand to Dave, with the site slug and who needs the login.
257
368
 
258
369
  ## Images — never hotlink, always commit the file
259
370
 
@@ -383,10 +494,21 @@ On a **form collection** specifically, since RFC 0004 moved submissions into Mon
383
494
  - **An `image` field is rejected** on a form collection. The old path uploaded a
384
495
  Visitor's file straight into the public repo, and a Mongo document is the wrong place
385
496
  for a blob. Deferred to RFC 0007 (object storage).
386
- - **`notify:` does not send email.** It runs, but through a `ConsoleMailer` that only
387
- logs — no address has ever received anything. Real delivery is RFC 0009. Don't
388
- promise a client they'll be emailed on a new submission; the inbox and its
389
- `needs-you` badge are the actual notification today.
497
+ - **New submissions are emailed** (RFC 0009, on RFC 0021's mailer), and
498
+ `notify:` is no longer how a *person* subscribes. Who gets one:
499
+ - **Site users on that Site are subscribed by default** — a lead nobody hears
500
+ about is the bug this closes, so the people whose business it is are opted in
501
+ unless they opt out.
502
+ - **Agency admins are not**, because they can reach every Site. They opt in per
503
+ Site per collection, under **Your Profile → Email notifications** in the admin.
504
+ - **`notify: [...]` now means addresses with no Scribe account** — a shared
505
+ `office@` inbox. Don't put a Scribe user's address in it; they manage their own.
506
+
507
+ The mail is best-effort and never fails the Visitor's POST, and `Reply-To` is
508
+ the Visitor's own address when the form has a field named `email` — that exact
509
+ name is the only signal, so a form whose address field is called something else
510
+ gets no `Reply-To`. So a client *can* be promised email on a new submission. The
511
+ inbox and its `needs-you` badge remain the thing of record.
390
512
 
391
513
  Per field: `name`, `type`, `label`, `required`, `default`, `pattern`, `hidden`,
392
514
  `readonly`, `isBody` (this field is the markdown body, not frontmatter), `list`
@@ -1,8 +1,7 @@
1
1
  # Recipes
2
2
 
3
- Patterns for building an Astro site against Scribe as the backend. Per
4
- [RFC 0010](../../../docs/proposals/0010-agent-authored-forms.md) these are
5
- **advice, not enforcement** — read the one you need, adapt it to the site you're
3
+ Patterns for building an Astro site against Scribe as the backend. Per RFC
4
+ 0010 these are **advice, not enforcement** — read the one you need, adapt it to the site you're
6
5
  building, and ignore the rest. There is no template to conform to and no
7
6
  generated config to keep in sync.
8
7
 
@@ -12,6 +11,14 @@ generated config to keep in sync.
12
11
  | [`collections.md`](./collections.md) | rendering content (posts, products, events) that editors manage in Scribe |
13
12
  | [`forms.md`](./forms.md) | putting one or more forms on the Site |
14
13
  | [`deploy.md`](./deploy.md) | setting up or debugging the build and deploy |
14
+ | [`domains.md`](./domains.md) | finding out whether a client has a domain, getting them one, the DNS, taking a Site live on it, or repairing one |
15
+ | [`requests.md`](./requests.md) | working the queue of jobs clients have asked the agency for |
16
+ | [`prospects.md`](./prospects.md) | recording and nurturing prospects in the calling pipeline |
17
+
18
+ `requests.md` and `prospects.md` are the odd ones out — they're about the
19
+ agency's own work (its queue and its sales pipeline) rather than anything in a
20
+ Site's repo, and they live here because the `scribe` CLI is the interface for
21
+ all of it.
15
22
 
16
23
  ## The part that isn't advice
17
24
 
@@ -0,0 +1,151 @@
1
+ # Taking a site live on a custom domain
2
+
3
+ One command does all of it. Reach for `scribe site promote` whenever the ask is
4
+ "publish the site at example.com", "point the domain at the site", "take it
5
+ live", or "set up the custom domain".
6
+
7
+ ```
8
+ scribe site promote <site> --domain example.com --dns-only # check DNS, change nothing
9
+ scribe site promote <site> --domain example.com # cut over
10
+ ```
11
+
12
+ ## Before the cutover: does the client even have a domain?
13
+
14
+ The agency user drives this whole thing themselves and is not a developer, so the
15
+ part that needs help is usually everything *before* `promote`. Work it in this
16
+ order, and say which step you're on.
17
+
18
+ **1. Find out what exists.** Don't take "we have a website" as "we have DNS we can
19
+ edit".
20
+
21
+ ```
22
+ dig +short NS example.com # who runs the DNS — this is the one that matters
23
+ dig +short example.com # where the name points today, if anywhere
24
+ dig +short MX example.com # mail. See the warning below.
25
+ whois example.com | head -30 # registrar, expiry, status (may be privacy-masked)
26
+ ```
27
+
28
+ The registrar on the invoice and the company running the DNS are often different.
29
+ **Follow the nameservers**, because that is where the records have to be added.
30
+ A `whois` that finds nothing registered is also the availability answer.
31
+
32
+ **2. If they don't own one, the client buys it.** They pay, it is registered in
33
+ their name, and they own it outright — never buy it on their behalf and never put
34
+ it in the agency's registrar. Help with the choice (business name, `.com` or the
35
+ local equivalent first, short, sayable on the phone, no hyphens or clever
36
+ spellings, nothing that pins them to one service or street), check candidates with
37
+ `whois`, and suggest they turn on auto-renew. Then they register it and come back
38
+ with access.
39
+
40
+ **3. Get DNS access.** Either their registrar login, or a call where they click
41
+ while you read out the records. Either is fine; the second is often faster. What
42
+ they're looking for is the screen listing records by type (`A`, `CNAME`, `MX`,
43
+ `TXT`) with an "Add record" button — every registrar names that screen
44
+ differently ("DNS", "Advanced DNS", "Manage DNS", "Nameservers").
45
+
46
+ Teach the shape of the task, not one provider's clicks. There is no supported
47
+ registrar; the client uses whatever they already have.
48
+
49
+ **4. Protect their email before you touch anything.** If `dig MX` returns records,
50
+ that domain carries their mail. The cutover only needs `A`/`CNAME` records for
51
+ the website — **say out loud that the `MX` and mail-related `TXT` records stay
52
+ exactly as they are**, and then make sure that is true. Breaking a client's email
53
+ is far worse than a late website, and it is the one mistake in this flow with no
54
+ quick undo from your side.
55
+
56
+ **5. Mind the old site.** If the domain currently serves a live website, the
57
+ switch is publicly visible. Agree a time rather than surprising them mid-trading.
58
+
59
+ Only then move on to `--dns-only` and the cutover below.
60
+
61
+ ## You do not need GitHub admin rights
62
+
63
+ The Pages configuration call (`PUT /repos/<org>/<repo>/pages`) is made by the
64
+ **admin app**, using Scribe's org-scoped token — not by the signed-in user's
65
+ GitHub account. A site user with a write-scoped API key can run the whole
66
+ cutover. If you catch yourself checking whether someone can administer the repo,
67
+ you are off the supported path: go back to `scribe site promote`.
68
+
69
+ Corollary: **never** set any of this by hand — not `gh api .../pages`, not the
70
+ GitHub Pages settings page, not a hand-written `public/CNAME`. Each one alone
71
+ produces a broken site (see "Repairing a half-finished cutover" below).
72
+
73
+ ## What promote actually does
74
+
75
+ In order, each step asserting current state before it writes:
76
+
77
+ 1. Checks that DNS already resolves to GitHub Pages — and refuses to continue if
78
+ not, because step 2 kills the old project-page URL.
79
+ 2. Commits `public/CNAME`. The deploy workflow keys off that file: present means
80
+ Astro builds with `base: '/'`, absent means `base: '/<repo>'`.
81
+ 3. Registers the domain with GitHub Pages via the API. **Both step 2 and step 3
82
+ are required.** The repo's workflow uses `build_type: workflow`, and Pages
83
+ does not read `CNAME` out of a build artifact — so the file alone changes the
84
+ base path without ever requesting a certificate, and the API call alone
85
+ serves the site from the root with every asset still pointing at `/<repo>/`.
86
+ 4. Sets `PUBLIC_SCRIBE_API_URL` so the contact form reaches the admin app, and
87
+ clears `PUBLIC_BASE_PATH`.
88
+ 5. Updates the `Site` record — this is the hostname the public submissions
89
+ endpoint accepts CORS from.
90
+ 6. Adds the domain to the Turnstile widget's allowed-domain list.
91
+ 7. Re-enforces HTTPS, and dispatches a deploy.
92
+
93
+ ## DNS
94
+
95
+ `--dns-only` prints the exact records. In short:
96
+
97
+ | Hostname | Records |
98
+ |---|---|
99
+ | Bare domain (`example.com`) | all four GitHub Pages **A** records |
100
+ | Subdomain (`www.example.com`) | one **CNAME** → `<org>.github.io` |
101
+
102
+ **In Cloudflare the record must be DNS-only (grey cloud), not proxied.** The
103
+ orange-cloud proxy blocks the ACME challenge GitHub uses to issue the
104
+ certificate, and promote will refuse with a `cloudflare-proxied` message.
105
+
106
+ `www` is not automatic. If a bare domain is the target and the client also wants
107
+ `www.example.com` to work, add a `www` CNAME → `<org>.github.io` at the
108
+ registrar; GitHub then redirects it to the apex on its own. Nothing in Scribe
109
+ needs to change for that.
110
+
111
+ ## Re-running is the fix
112
+
113
+ Promotion is idempotent and there is deliberately no `site demote`. If a run
114
+ fails partway it prints which steps completed; fix the cause and run the same
115
+ command again.
116
+
117
+ The most common "failure" isn't one: setting a custom domain clears
118
+ `https_enforced`, and GitHub refuses to turn it back on until the certificate
119
+ exists. Promote retries at the end and warns if it isn't ready. Wait a few
120
+ minutes, re-run, done. Until then `http://` won't serve the site.
121
+
122
+ ## Repairing a half-finished cutover
123
+
124
+ The three pieces of state — the `Site` record's domain, the Pages custom domain,
125
+ and `public/CNAME` — can each be set independently, and nothing in
126
+ `scribe site verify` currently compares them. A half-state looks healthy and
127
+ serves nothing.
128
+
129
+ | Symptom | What's missing |
130
+ |---|---|
131
+ | Domain 404s, no certificate ever issued | Pages custom domain not registered |
132
+ | Page loads but every asset/link 404s | `public/CNAME` missing, so the build still uses `base=/<repo>` |
133
+ | Old `github.io/<repo>` URL broke and the new domain isn't up | `public/CNAME` landed before the Pages domain was set |
134
+ | Form rejected with a CORS error on the new host | `Site` record still names the old domain |
135
+
136
+ The fix for all four is the same: `scribe site promote <site> --domain
137
+ example.com`. It reconciles whatever is already correct and writes the rest.
138
+
139
+ ## `site update --domain` is not this
140
+
141
+ ```
142
+ scribe site update <site> --domain example.com # record + Turnstile ONLY
143
+ ```
144
+
145
+ It rewrites the stored hostname and syncs the Turnstile widget. It does not
146
+ touch DNS, `public/CNAME`, or Pages. Using it to go live leaves a site that
147
+ claims a domain serving nothing — the exact half-state above. The CLI now
148
+ refuses when it detects a cutover (a site still on `<org>.github.io` being moved
149
+ to a real hostname) and points at `promote`; `--record-only` overrides it.
150
+
151
+ Legitimate use: correcting a typo in a domain that is already live.
@@ -310,8 +310,11 @@ inbox already saying so.
310
310
 
311
311
  - **Two forms, one page:** fine, as long as element ids are unique. The snippet
312
312
  above scopes everything to `scribe-form-<collection>`.
313
- - **`notify:` does not send email.** It's parsed and logged by a console mailer
314
- only. Don't promise a client notifications (RFC 0009 is the real thing).
313
+ - **New submissions are emailed** (RFC 0009), but `notify:` is only for addresses
314
+ with **no Scribe account** — a shared `office@` inbox. Site users on the Site are
315
+ subscribed by default and manage it themselves under Your Profile → Email
316
+ notifications; agency admins are opted out by default and opt in per Site. Adding
317
+ a Scribe user's address to `notify:` is redundant, not helpful.
315
318
  - **Rate limiting is per Site and IP, not per form**, so hammering one form while
316
319
  testing will also throttle the others.
317
320
  - **An unchecked checkbox in `FormData` is absent, not `false`.** The payload