@taprootio/site-authoring 0.15.0 → 0.18.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.
- package/README.md +171 -11
- package/package.json +1 -1
- package/src/api.js +194 -11
- package/src/authors-contract.js +170 -0
- package/src/cli.js +252 -12
- package/src/constants.js +19 -0
- package/src/content/link-warnings.js +109 -0
- package/src/fixture-init.js +55 -3
- package/src/page-metadata.js +92 -0
- package/src/reference-help.js +58 -8
- package/src/staging-check.js +62 -0
- package/src/transport.js +73 -12
- package/src/typed-page-reference.js +89 -19
- package/src/typed-pages.js +165 -4
- package/src/verbs/approve.js +63 -14
- package/src/verbs/authors.js +167 -0
- package/src/verbs/deploy.js +29 -32
- package/src/verbs/index.js +18 -1
- package/src/verbs/media-upload.js +0 -1
- package/src/verbs/pages-meta-set.js +303 -0
- package/src/verbs/pages-push.js +419 -36
- package/src/verbs/places.js +72 -2
- package/src/verbs/plan.js +11 -1
- package/src/verbs/pull.js +255 -20
- package/src/verbs/redirects-check.js +4 -10
- package/src/verbs/status.js +1 -4
- package/src/verbs/validate.js +47 -2
- package/src/workspace.js +58 -6
package/README.md
CHANGED
|
@@ -124,8 +124,12 @@ with no credential, no network, and no write: every page source (metadata,
|
|
|
124
124
|
Markdown conversion, media references, content vocabulary, section contexts),
|
|
125
125
|
page paths and templates against the pages the pull recorded, navigation,
|
|
126
126
|
redirects, forms, and the complete theme, appearance, header, brand, and
|
|
127
|
-
footer.
|
|
128
|
-
|
|
127
|
+
footer. Navigation to a generated page (a section, tag or place page) resolves
|
|
128
|
+
against the pages the pull recorded. It reports every problem at once in
|
|
129
|
+
`error.problems`, each with its file, code, and field, instead of one per run.
|
|
130
|
+
Warnings never refuse: `linkWarnings` names a link whose address is only
|
|
131
|
+
`http(s)://` plus its own text (`content.link_autolinked`, left by older editor
|
|
132
|
+
autolinking), and `pages push` reports the same for the pages it sends.
|
|
129
133
|
|
|
130
134
|
`taproot-site validate <fixture-directory>` checks a complete offline fixture
|
|
131
135
|
the same way: a directory laid out like a
|
|
@@ -360,6 +364,92 @@ it is created: a source for another template is refused with
|
|
|
360
364
|
`pages.template_immutable`. `taproot-site help page <type>` gives each
|
|
361
365
|
template's fields and one worked example.
|
|
362
366
|
|
|
367
|
+
A `.pm.json` page's title, path, description and author live in the workspace
|
|
368
|
+
manifest rather than the file. `pages meta set <path> --title <text>
|
|
369
|
+
--description <text>` changes the title or description there (a Markdown page
|
|
370
|
+
keeps them in its front matter; `--author` is described under *Page authors*); `pull` keeps that edit until `pages push`
|
|
371
|
+
sends it, and a site edit to the same page is a `pages.pull_conflict`. A
|
|
372
|
+
description over 160 characters is a warning (`descriptionWarnings`), because
|
|
373
|
+
search results cut it off; over 1000 is refused with
|
|
374
|
+
`pages.description_too_long`, by the CLI and by Taproot.
|
|
375
|
+
|
|
376
|
+
### Page authors
|
|
377
|
+
|
|
378
|
+
Pages are authorless by default: a page you create with the CLI publishes with
|
|
379
|
+
no byline unless you name an author, and `pages push` lists the pages it created
|
|
380
|
+
that way in `pages.authorless`, with how to credit them. An author is one of two
|
|
381
|
+
kinds of person, named by one string:
|
|
382
|
+
|
|
383
|
+
- a **site author without a Taproot account** — one of an imported blog's
|
|
384
|
+
authors, say — named by handle (`jane-doe`: lowercase letters, digits and
|
|
385
|
+
single hyphens, at most 64 characters, fixed when the author is created). A
|
|
386
|
+
site author's name appears on the byline and in the page's structured data;
|
|
387
|
+
there is no profile link or follow button, because there is no account;
|
|
388
|
+
- a **site member who can create pages**, named by email address.
|
|
389
|
+
|
|
390
|
+
`authors list` prints both and rewrites `authors.json` (`pull` writes it too).
|
|
391
|
+
`authors add jane-doe --name "Jane Doe" [--email jane@example.com]` creates a
|
|
392
|
+
site author; `--email` is kept private (never published, never listed) so the
|
|
393
|
+
person can be linked to a Taproot account later. Adding a handle the site
|
|
394
|
+
already has under the same name succeeds and reports `existing: true`, so a
|
|
395
|
+
script can repeat itself; a different name for a taken handle is refused
|
|
396
|
+
(`authors.handle_taken`), naming the one the site has. `authors.json` holds the
|
|
397
|
+
members' email addresses, so keep it out of a public repository.
|
|
398
|
+
|
|
399
|
+
A page names its author with `author: jane-doe` in its front matter (any
|
|
400
|
+
template), or, for a `.pm.json` page, `pages meta set <path> --author jane-doe`.
|
|
401
|
+
Only the CLI assigns authors, and only to a page that has none; people editing
|
|
402
|
+
in the app are never asked to name one. `authors.json` is as old as the last
|
|
403
|
+
`pull` or `authors list`, so a name missing from it is not proof the person does
|
|
404
|
+
not exist. `validate` and `pages meta set` run offline: they refuse text that
|
|
405
|
+
is neither a handle nor an address (`pages.author_invalid`) and otherwise only
|
|
406
|
+
warn (`pages.author_unverified`, in `authorWarnings`) about a name `authors.json`
|
|
407
|
+
lacks. `pages push` and `plan` settle it online: on a miss they read the site's
|
|
408
|
+
current list once (`pages push` rewrites `authors.json` with it) and refuse
|
|
409
|
+
(`pages.author_unknown`) only a name the site really lacks. A member who cannot
|
|
410
|
+
create pages is `pages.author_unknown` as well, with the same wording.
|
|
411
|
+
|
|
412
|
+
The CLI only fills a page that has no author: a page that already has a
|
|
413
|
+
different one is refused (`pages.author_conflict`), by the CLI from what the
|
|
414
|
+
last pull recorded and by Taproot, and naming the author a page already has
|
|
415
|
+
changes nothing. The refusal says how to settle it: for a `.pm.json` page,
|
|
416
|
+
`pages meta set <path> --author <the site's author>` matches the site, and
|
|
417
|
+
`pages meta set <path> --author ""` drops an author that was never sent (it
|
|
418
|
+
never asks the site to remove one, and is refused once the site holds an
|
|
419
|
+
author); for a Markdown page, fix the `author:` line. `pull` keeps a pending
|
|
420
|
+
author the site now disagrees with and says so. The author is recorded on the
|
|
421
|
+
page's manifest entry (`author`, and `baseline.author` for what the site
|
|
422
|
+
held), so a re-push never resends it. The site names a page's author in
|
|
423
|
+
listings only while that person can still be named: a former member's page, or
|
|
424
|
+
a member who can no longer create pages, reports none, and a push that names
|
|
425
|
+
someone else on it is refused as a conflict.
|
|
426
|
+
|
|
427
|
+
Pages Taproot generates from the site's content — a tag, the tags index, a
|
|
428
|
+
published year or month, a place archive, a folder — are update-only sources.
|
|
429
|
+
`pull` writes each as `pages/<name>.pm.json` with `"template": "generated"`:
|
|
430
|
+
`data` holds what the page is (`kind`, `tagId`, `year`, `month`, `countryCode`,
|
|
431
|
+
`regionCode`, `citySlug`, `categorySlug`, which you leave as pulled) and the four
|
|
432
|
+
things that are yours to author: `customTitle`, `breadcrumbTitle`,
|
|
433
|
+
`customDescription` and `introductionBody`. An empty string means "use the
|
|
434
|
+
generated default"; the description then falls back to the introduction's first
|
|
435
|
+
paragraph, else the system default. The manifest keeps the title and description
|
|
436
|
+
the site reports, for reference. `pages meta set <path> --title/--description`
|
|
437
|
+
edits `customTitle` and `customDescription` in the source (an empty value
|
|
438
|
+
clears it), and `pages push` sends the page back as an unapproved draft. The
|
|
439
|
+
title and description the site stores are derived from `customTitle`,
|
|
440
|
+
`customDescription` and `introductionBody`, so what a push sends for them is
|
|
441
|
+
ignored. A push refuses to create a generated page (`pages.generated_create`),
|
|
442
|
+
move one (`pages.generated_move`) or change its kind or identity
|
|
443
|
+
(`pages.generated_identity`, also when the manifest has no record of what the
|
|
444
|
+
file was pulled as: run `pull`); there is no Markdown form
|
|
445
|
+
(`pages.generated_markdown`). A generated page you did not edit never blocks a
|
|
446
|
+
push of the whole workspace, even when Taproot moved it since the pull (a tag was
|
|
447
|
+
renamed). If the site no longer has the page (its tag, month or folder is gone),
|
|
448
|
+
a whole-workspace push reports the file under `pages.staleGeneratedSources` and
|
|
449
|
+
sends nothing for it, and `pull` says so; delete the file. Naming that path in
|
|
450
|
+
`pages push <page-path>` is refused with `pages.generated_create`. A description over 160 characters is warned about,
|
|
451
|
+
since search results cut it off, and one over 1000 is refused.
|
|
452
|
+
|
|
363
453
|
`pages push [page-path...]` validates and sends the selected pages, or every
|
|
364
454
|
workspace page when none is named; the homepage is addressed as `/`. Every
|
|
365
455
|
selected page is checked before anything is sent, and a refusal lists every
|
|
@@ -374,7 +464,8 @@ moved since this workspace last reconciled with it, naming both revisions;
|
|
|
374
464
|
names and reports predictions with a session token; `places select
|
|
375
465
|
<googlePlaceId> <sessionToken>` records the match as a Taproot place and
|
|
376
466
|
reports the `placeId` a place review's front matter needs. An article or an
|
|
377
|
-
album can name its place the same way with `place:`.
|
|
467
|
+
album can name its place the same way with `place:`. `places category set
|
|
468
|
+
<placeId> <category>` changes the category this site shows for a place.
|
|
378
469
|
|
|
379
470
|
`plan` checks the whole workspace against the live site — pages, the media
|
|
380
471
|
they reference and the images the settings name, the footer and the pages it
|
|
@@ -477,8 +568,10 @@ paused; retry later), `credential_rejected` (stop and re-issue),
|
|
|
477
568
|
not exchanged for a capability the request needs; the refusal carries
|
|
478
569
|
`field: "GrantedCapabilities"`, and the CLI's human progress names the
|
|
479
570
|
granted and required capabilities, so report the verb's declared set rather
|
|
480
|
-
than re-issuing the key), `plan_limit` (the plan's published-page allowance
|
|
481
|
-
|
|
571
|
+
than re-issuing the key), `plan_limit` (the plan's published-page allowance or
|
|
572
|
+
its pages-generated allowance, or the bandwidth pause on deploys,
|
|
573
|
+
surfaced at deploy; the allowance shows its reset time, while the bandwidth pause shows the earliest
|
|
574
|
+
time it can lift, since it lifts only after a month ends under the allowance or on upgrade), and `throttled` (back off).
|
|
482
575
|
|
|
483
576
|
When `GITHUB_OUTPUT` names the runner's existing output file, every
|
|
484
577
|
operational verb appends the same JSON under `taproot_site_result` through a
|
|
@@ -545,6 +638,69 @@ Other C0/C1 controls, U+2028/U+2029, U+FEFF, and literal `<br>` tags are rejecte
|
|
|
545
638
|
with the exact fact field path. A label is optional and stays outside the value's link. See
|
|
546
639
|
`taproot-site help page free-form` and `taproot-site help component cta`.
|
|
547
640
|
|
|
641
|
+
### Place categories in 0.18.0
|
|
642
|
+
|
|
643
|
+
A place has one category for every site, taken from Google's types, and a place
|
|
644
|
+
that Taproot could not classify is filed under `Other`. A site can file a place
|
|
645
|
+
under a category of its own:
|
|
646
|
+
|
|
647
|
+
```
|
|
648
|
+
taproot-site places category list
|
|
649
|
+
taproot-site places category set <placeId> park
|
|
650
|
+
taproot-site places category clear <placeId>
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
`list` prints the closed list `set` accepts; a name outside it is refused, and
|
|
654
|
+
capitalization is ignored. The category is the site's alone and applies to every
|
|
655
|
+
review of that place on the site: the place itself and other sites do not
|
|
656
|
+
change. Place-review bylines and the `/places/<category>` pages use it from the
|
|
657
|
+
site's next deployment, including reviews nobody has edited. `clear` removes it.
|
|
658
|
+
Both need the same page-editing permission as `places select`.
|
|
659
|
+
|
|
660
|
+
### Pages generated in 0.18.0
|
|
661
|
+
|
|
662
|
+
A site's licence allows a number of pages generated per billing period, and
|
|
663
|
+
`deploy --staging` is charged by the pages it generates, not by the deployment.
|
|
664
|
+
The deploy asks Taproot to plan the candidate first and reports the plan before
|
|
665
|
+
it sends anything: `This will generate 74 pages · 940 of 1,000 left this period
|
|
666
|
+
(resets 2026-11-01T00:00:00.000Z)`. The same figures are in `readiness.pageEstimate`
|
|
667
|
+
(`plannedPageCount`, `pagesGeneratedLimit`, `pagesGeneratedRemaining`, `periodEnd`
|
|
668
|
+
and `exceedsAllowance`; a limit and what remains are `null` on an unlimited
|
|
669
|
+
licence). The number shown is the number charged. Promoting a staged release to
|
|
670
|
+
production generates nothing and costs no pages.
|
|
671
|
+
|
|
672
|
+
A candidate that does not fit stops with `deploy.page_allowance_exceeded`, naming
|
|
673
|
+
the pages it needs, the pages left, and the reset. A deploy refused by the server
|
|
674
|
+
for the same reason, or for the bandwidth pause, reports `plan_limit` with the
|
|
675
|
+
time it can lift (and `requested_pages` and `remaining_pages` for the allowance):
|
|
676
|
+
the allowance's reset, or for the bandwidth pause the earliest month end that can
|
|
677
|
+
clear it (an upgrade clears it at once).
|
|
678
|
+
|
|
679
|
+
### Deployment allowances in 0.18.0
|
|
680
|
+
|
|
681
|
+
A deploy refused for the bandwidth pause reports `plan_limit` with the time it
|
|
682
|
+
can lift: the earliest month end that can clear it (an upgrade clears it at
|
|
683
|
+
once).
|
|
684
|
+
|
|
685
|
+
### Authoring feedback in 0.16.0
|
|
686
|
+
|
|
687
|
+
- `pages meta set <path> --title/--description/--author` edits a `.pm.json`
|
|
688
|
+
page's title, description or author; `pull` keeps the edit until `pages push`
|
|
689
|
+
sends it. Descriptions are warned about over 160 characters and refused over
|
|
690
|
+
1000, here and by Taproot.
|
|
691
|
+
- Generated pages (tags, archives, places, folders) pull as update-only sources
|
|
692
|
+
whose custom title, breadcrumb, description and introduction you can edit.
|
|
693
|
+
- Pages are authorless by default. `authors list`, `authors add` and
|
|
694
|
+
`authors.json` name a member who can create pages or a site author without a
|
|
695
|
+
Taproot account; `author:` front matter or `pages meta set --author` credits
|
|
696
|
+
one on a page that has none, and an author is never replaced from the CLI.
|
|
697
|
+
- `redirects check` and `deploy --staging` report `routeCheck`, `redirects` and
|
|
698
|
+
`stagingPreview` at the top level; result paths spell the home page `/`;
|
|
699
|
+
`approve` skips a page with nothing to approve instead of failing the batch.
|
|
700
|
+
- `validate` checks a real pulled workspace offline (`validate --init` keeps the
|
|
701
|
+
generated pages navigation links to), and `validate` and `pages push` warn on
|
|
702
|
+
links that look made by autolinking (`content.link_autolinked`).
|
|
703
|
+
|
|
548
704
|
### Link rel in 0.15.0
|
|
549
705
|
|
|
550
706
|
A link publishes followed unless its author chooses otherwise. A link mark's
|
|
@@ -579,13 +735,15 @@ only error class and known page/node/attribute labels, never renderer messages.
|
|
|
579
735
|
Staging deploys check the current redirect map through the real staging edge.
|
|
580
736
|
Run `taproot-site redirects check` to repeat once the deployment has completed; a redirect is a file inside the release, so it is live exactly when its release is served.
|
|
581
737
|
The Node CLI consumes a separate handoff, retains its short staging cookie in
|
|
582
|
-
memory, and requests each map path without following redirects.
|
|
583
|
-
|
|
738
|
+
memory, and requests each map path without following redirects. Both
|
|
739
|
+
`redirects check` and `deploy --staging` report `routeCheck` and `redirects` at
|
|
740
|
+
the top level of their result: `redirects.items` lists each path, HTTP status
|
|
741
|
+
and Location, and `redirects.verified` requires all entries to match and the
|
|
584
742
|
map revision to remain unchanged. A mismatch is evidence to investigate, not a
|
|
585
743
|
failed deployment. Checks describe the current map, not a historical release.
|
|
586
744
|
Requests have a 90-second deadline with eight concurrent requests; large JSON
|
|
587
|
-
lists have explicit truncation, prioritizing failures.
|
|
588
|
-
entry. A gate/transport failure never counts as verified.
|
|
745
|
+
lists have explicit truncation, prioritizing failures. Progress output reports
|
|
746
|
+
each entry. A gate/transport failure never counts as verified.
|
|
589
747
|
|
|
590
748
|
`stagingPreview.url` is a fresh single-use two-minute handoff minted **after**
|
|
591
749
|
checks. Opening it consumes it and establishes a five-minute staging cookie.
|
|
@@ -640,8 +798,10 @@ The destination must be new, outside the source workspace, and its parent must e
|
|
|
640
798
|
to select another source configuration. Initialization needs no credential or
|
|
641
799
|
network. It exports editable pages of every template as workspace documents with navigation,
|
|
642
800
|
redirects, settings, and version-7 appearance/footer metadata, validates the
|
|
643
|
-
result, and leaves the source unchanged.
|
|
644
|
-
|
|
801
|
+
result, and leaves the source unchanged. A metadata-only page that navigation,
|
|
802
|
+
the footer or a page links to, such as a generated section page, is kept as a
|
|
803
|
+
metadata-only entry (`initialized.linkedPages`); other metadata-only pages are
|
|
804
|
+
left out (`initialized.skippedPages`).
|
|
645
805
|
|
|
646
806
|
Exported UUIDs and HTTP(S) origins are deterministic fixture values under
|
|
647
807
|
`example.test`; URL credentials, query strings and fragments are removed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
package/src/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeLatestCliVersion } from "./cli-release.js";
|
|
2
2
|
import {
|
|
3
|
+
BANDWIDTH_ALLOWANCE_REFUSAL_REASON,
|
|
3
4
|
CANONICAL_TIMESTAMP,
|
|
4
5
|
CAPABILITY_REFUSAL_FIELD,
|
|
5
6
|
CLI_UPGRADE_COMMAND,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
// would turn a successful mint into an orphaned key.
|
|
24
25
|
import { KEY_PREFIX } from "./credentials.js";
|
|
25
26
|
import { isCanonicalUuid, normalizePreviewDiagnostic, sanitizeDiagnostic, SiteAuthoringError } from "./errors.js";
|
|
27
|
+
import { normalizeAuthorListing, parseAuthorReference } from "./authors-contract.js";
|
|
26
28
|
import { docsPublicationModeFromWire, siteKindFromWire, surfaceFromWire } from "./surface.js";
|
|
27
29
|
import {
|
|
28
30
|
DEFAULT_REDIRECT_STATUS,
|
|
@@ -436,6 +438,34 @@ export function announceRefusal(error, onProgress, action = "request") {
|
|
|
436
438
|
}
|
|
437
439
|
if (kind === REFUSAL_PLAN_LIMIT) {
|
|
438
440
|
onProgress("");
|
|
441
|
+
if (error.planLimit !== undefined) {
|
|
442
|
+
// The allowance and bandwidth refusals ride an ErrorInfo detail, not the
|
|
443
|
+
// UpgradePrompt field, and name when they can next lift on their own.
|
|
444
|
+
const bandwidth = error.planLimit.reason === BANDWIDTH_ALLOWANCE_REFUSAL_REASON;
|
|
445
|
+
onProgress(`PLAN LIMIT (refusal=plan_limit, reason=${error.planLimit.reason})`);
|
|
446
|
+
onProgress(
|
|
447
|
+
bandwidth
|
|
448
|
+
? ` Taproot paused this ${action}: the site's measured bandwidth was over its monthly allowance for two `
|
|
449
|
+
+ "months in a row. The published site keeps serving; only new deployments wait."
|
|
450
|
+
: error.planLimit.requestedPages === undefined
|
|
451
|
+
? ` Taproot refused this ${action}: it generates more pages than the site's allowance for this billing period has left.`
|
|
452
|
+
: ` Taproot refused this ${action}: it generates ${pagesLabel(error.planLimit.requestedPages)} and the site has ${
|
|
453
|
+
(error.planLimit.remainingPages ?? 0).toLocaleString("en-US")
|
|
454
|
+
} left for this billing period.`,
|
|
455
|
+
);
|
|
456
|
+
// The bandwidth pause is not an unconditional reset: it lifts only after
|
|
457
|
+
// a month ends under the allowance (or on upgrade), so the instant is
|
|
458
|
+
// the earliest that can happen.
|
|
459
|
+
onProgress(
|
|
460
|
+
error.planLimit.resetsAt === undefined
|
|
461
|
+
? " Upgrade the site's plan, then run the request again."
|
|
462
|
+
: bandwidth
|
|
463
|
+
? ` Upgrade the site's plan, or deployments resume after a month ends under the allowance (no earlier than ${error.planLimit.resetsAt}).`
|
|
464
|
+
: ` Resets at: ${error.planLimit.resetsAt}. Upgrade the site's plan to continue sooner.`,
|
|
465
|
+
);
|
|
466
|
+
onProgress("");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
439
469
|
onProgress("PLAN LIMIT (refusal=plan_limit, field=UpgradePrompt)");
|
|
440
470
|
onProgress(
|
|
441
471
|
` Taproot refused this ${action} against a plan ceiling rather than a content problem.`,
|
|
@@ -499,6 +529,14 @@ export function normalizePageSummary(value) {
|
|
|
499
529
|
// The page's calendar date, empty when it has none. It is not part of
|
|
500
530
|
// `bodyRevision`, so a typed page's push guard compares it separately.
|
|
501
531
|
displayDate: typeof summary.displayDate === "string" ? summary.displayDate : "",
|
|
532
|
+
// The name the page's author goes by in a write: a site author's handle or
|
|
533
|
+
// a member's email address, folded. Empty for a page with no author, for a
|
|
534
|
+
// former member's page, and against a Taproot that predates authors
|
|
535
|
+
// (TR01196). Never the author's display name, which is not a reference.
|
|
536
|
+
authorRef: typeof summary.authorRef === "string" ? parseAuthorReference(summary.authorRef)?.value ?? "" : "",
|
|
537
|
+
// Whether the page credits anyone at all, which an empty authorRef cannot
|
|
538
|
+
// say: it is also empty for an author this credential may not name.
|
|
539
|
+
hasAuthor: typeof summary.authorDisplayName === "string" && summary.authorDisplayName.trim() !== "",
|
|
502
540
|
};
|
|
503
541
|
}
|
|
504
542
|
|
|
@@ -545,6 +583,62 @@ export async function updatePage(client, pageId, body) {
|
|
|
545
583
|
);
|
|
546
584
|
}
|
|
547
585
|
|
|
586
|
+
/**
|
|
587
|
+
* Gives the site's two author refusals the stable codes the CLI documents
|
|
588
|
+
* (TR01196). The site answers an unknown name and one that cannot be named with
|
|
589
|
+
* the same field and wording on purpose; this keeps them one code too.
|
|
590
|
+
*/
|
|
591
|
+
export function translateAuthorRefusal(error, file) {
|
|
592
|
+
if (!(error instanceof ApiError)) return error;
|
|
593
|
+
for (const [field, code, fallback] of [
|
|
594
|
+
["AuthorUnknown", "pages.author_unknown", "No site author or member who can create pages has that name."],
|
|
595
|
+
["AuthorConflict", "pages.author_conflict", "The page already has a different author, which is never replaced."],
|
|
596
|
+
]) {
|
|
597
|
+
if (error.hasField(field)) {
|
|
598
|
+
const detail = sanitizeDiagnostic(error.descriptionFor(field) ?? fallback, fallback);
|
|
599
|
+
// The workspace only learns who the site credits from a pull, and a
|
|
600
|
+
// conflict means someone gave the page an author after the last one.
|
|
601
|
+
const hint = field === "AuthorConflict"
|
|
602
|
+
? " Run 'taproot-site pull' to learn who the site credits, then match it with 'pages meta set <path> --author "
|
|
603
|
+
+ "<author>' (a .pm.json page) or the author: front matter line (Markdown)."
|
|
604
|
+
: "";
|
|
605
|
+
return new SiteAuthoringError(code, `'${file}': ${detail}${hint}`, { field: file });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return error;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Reads who a page can be credited to: the site's authors without an account
|
|
613
|
+
* and the members who can create pages (TR01196).
|
|
614
|
+
*/
|
|
615
|
+
export async function listSiteAuthors(client, siteId) {
|
|
616
|
+
return normalizeAuthorListing(
|
|
617
|
+
requireObject(await client.request(sitePath(siteId, "authors")), "api.authors_contract", "site authors"),
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Creates a site author. The site folds the handle and the address. */
|
|
622
|
+
export async function createSiteAuthor(client, siteId, { handle, displayName, email }) {
|
|
623
|
+
const created = requireObject(
|
|
624
|
+
await client.request(sitePath(siteId, "authors"), {
|
|
625
|
+
method: "POST",
|
|
626
|
+
body: { handle, displayName, ...(email === undefined ? {} : { email }) },
|
|
627
|
+
}),
|
|
628
|
+
"api.authors_contract",
|
|
629
|
+
"site author",
|
|
630
|
+
);
|
|
631
|
+
const listing = normalizeAuthorListing({ authors: [created], members: [] });
|
|
632
|
+
if (listing.authors.length !== 1) {
|
|
633
|
+
throw new SiteAuthoringError(
|
|
634
|
+
"api.authors_contract",
|
|
635
|
+
"Taproot returned a site author this CLI could not read.",
|
|
636
|
+
{ field: "handle" },
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
return listing.authors[0];
|
|
640
|
+
}
|
|
641
|
+
|
|
548
642
|
export async function publishDrafts(client, pageIds) {
|
|
549
643
|
const response = requireObject(
|
|
550
644
|
await client.request("v1/pages/publish_drafts", { method: "POST", body: { pageIds } }),
|
|
@@ -931,6 +1025,45 @@ export async function selectPlace(client, siteId, { googlePlaceId, sessionToken
|
|
|
931
1025
|
};
|
|
932
1026
|
}
|
|
933
1027
|
|
|
1028
|
+
function normalizeSiteCategory(value) {
|
|
1029
|
+
const state = requireObject(value, "api.place_contract", "site place category");
|
|
1030
|
+
return {
|
|
1031
|
+
placeId: requireIdentifier(state.placeId, "api.place_contract", "placeId"),
|
|
1032
|
+
category: text(state.category),
|
|
1033
|
+
placeCategory: text(state.placeCategory),
|
|
1034
|
+
isOverride: state.isOverride === true,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** Sets this site's own category for a place; the place itself and other sites are unchanged. */
|
|
1039
|
+
export async function setPlaceCategory(client, siteId, { placeId, category }) {
|
|
1040
|
+
return normalizeSiteCategory(
|
|
1041
|
+
await client.request(sitePath(siteId, `places/${encodeURIComponent(placeId)}/category`), {
|
|
1042
|
+
method: "PUT",
|
|
1043
|
+
body: { siteId, placeId, category },
|
|
1044
|
+
}),
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** Removes this site's category for a place, so the place's own category applies again. */
|
|
1049
|
+
export async function clearPlaceCategory(client, siteId, { placeId }) {
|
|
1050
|
+
return normalizeSiteCategory(
|
|
1051
|
+
await client.request(sitePath(siteId, `places/${encodeURIComponent(placeId)}/category`), { method: "DELETE" }),
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** The closed list of place categories, in Taproot's order. */
|
|
1056
|
+
export async function listPlaceCategories(client) {
|
|
1057
|
+
const response = requireObject(
|
|
1058
|
+
await client.request(`v1/standard-types${query([["typeGroup", "place_category"], ["maxResults", 1000]])}`),
|
|
1059
|
+
"api.place_contract",
|
|
1060
|
+
"place categories",
|
|
1061
|
+
);
|
|
1062
|
+
return (Array.isArray(response.standardTypes) ? response.standardTypes : [])
|
|
1063
|
+
.map((type) => text(requireObject(type, "api.place_contract", "place category").name))
|
|
1064
|
+
.filter((name) => name !== "");
|
|
1065
|
+
}
|
|
1066
|
+
|
|
934
1067
|
/**
|
|
935
1068
|
* Processing is observed through `ListSiteImages`; `GetImageById` is
|
|
936
1069
|
* session-only under TR00602's read list, so a key-authorized client watches
|
|
@@ -1057,18 +1190,20 @@ export async function getDeploySelection(client, siteId) {
|
|
|
1057
1190
|
}
|
|
1058
1191
|
|
|
1059
1192
|
export async function getPublishingReadiness(client, siteId, selection = {}) {
|
|
1193
|
+
// A body, not a query: the exact selection can run past a URL's limit, and the
|
|
1194
|
+
// estimate must plan the pages the deploy will stage (TR01201).
|
|
1060
1195
|
const response = requireObject(
|
|
1061
|
-
await client.request(sitePath(
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}
|
|
1071
|
-
)
|
|
1196
|
+
await client.request(sitePath(siteId, "publishing/readiness"), {
|
|
1197
|
+
method: "POST",
|
|
1198
|
+
body: {
|
|
1199
|
+
stagedPageIds: selection.stagedPageIds,
|
|
1200
|
+
selectedSettingsTypes: selection.selectedSettingsTypes,
|
|
1201
|
+
includeNavigation: selection.includeNavigation,
|
|
1202
|
+
useExactPageSelection: selection.useExactPageSelection,
|
|
1203
|
+
includePageEstimate: selection.includePageEstimate,
|
|
1204
|
+
allowFailedPreview: selection.allowFailedPreview,
|
|
1205
|
+
},
|
|
1206
|
+
}),
|
|
1072
1207
|
"api.readiness_contract",
|
|
1073
1208
|
"publishing readiness",
|
|
1074
1209
|
);
|
|
@@ -1089,10 +1224,50 @@ export async function getPublishingReadiness(client, siteId, selection = {}) {
|
|
|
1089
1224
|
redirectsChanged: response.redirectsChanged === true,
|
|
1090
1225
|
formsChanged: response.formsChanged === true,
|
|
1091
1226
|
videosChanged: response.videosChanged === true,
|
|
1227
|
+
// The pages staging this candidate would generate, which is the number charged (TR01201).
|
|
1228
|
+
// Absent unless asked for and the candidate could be planned.
|
|
1229
|
+
pageEstimate: pageEstimate(response),
|
|
1092
1230
|
blockers,
|
|
1093
1231
|
};
|
|
1094
1232
|
}
|
|
1095
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* The server's plan for a candidate: the pages staging it generates, the
|
|
1236
|
+
* allowance left this period and when it resets. The CLI never computes any of
|
|
1237
|
+
* it; limit, remaining and period end are null for an unlimited licence.
|
|
1238
|
+
*/
|
|
1239
|
+
function pageEstimate(response) {
|
|
1240
|
+
if (!Number.isSafeInteger(response.plannedPageCount) || response.plannedPageCount < 0) return null;
|
|
1241
|
+
const periodEnd = typeof response.pagesGeneratedPeriodEnd === "string"
|
|
1242
|
+
? new Date(response.pagesGeneratedPeriodEnd)
|
|
1243
|
+
: undefined;
|
|
1244
|
+
return {
|
|
1245
|
+
plannedPageCount: response.plannedPageCount,
|
|
1246
|
+
pagesGeneratedLimit: Number.isSafeInteger(response.pagesGeneratedLimit) ? response.pagesGeneratedLimit : null,
|
|
1247
|
+
pagesGeneratedRemaining: Number.isSafeInteger(response.pagesGeneratedRemaining)
|
|
1248
|
+
? response.pagesGeneratedRemaining
|
|
1249
|
+
: null,
|
|
1250
|
+
periodEnd: periodEnd !== undefined && !Number.isNaN(periodEnd.getTime()) ? periodEnd.toISOString() : null,
|
|
1251
|
+
exceedsAllowance: response.plannedPagesExceedAllowance === true,
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const pagesLabel = (count) => `${count.toLocaleString("en-US")} ${count === 1 ? "page" : "pages"}`;
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* The one line that states a candidate's plan in pages (TR01201), shared by the
|
|
1259
|
+
* progress output and the refusal so the two never word it differently.
|
|
1260
|
+
*/
|
|
1261
|
+
export function describePageEstimate(estimate) {
|
|
1262
|
+
const generates = `This will generate ${pagesLabel(estimate.plannedPageCount)}`;
|
|
1263
|
+
if (estimate.pagesGeneratedLimit === null || estimate.pagesGeneratedRemaining === null) return generates;
|
|
1264
|
+
const left = `${estimate.pagesGeneratedRemaining.toLocaleString("en-US")} of ${
|
|
1265
|
+
estimate.pagesGeneratedLimit.toLocaleString("en-US")
|
|
1266
|
+
} left this period`;
|
|
1267
|
+
const resets = estimate.periodEnd === null ? "" : ` (resets ${estimate.periodEnd})`;
|
|
1268
|
+
return `${generates} · ${left}${resets}`;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1096
1271
|
export async function getStagingPreviewStatus(client, siteId) {
|
|
1097
1272
|
const response = requireObject(
|
|
1098
1273
|
await client.request(sitePath(siteId, "staging-preview/status")),
|
|
@@ -1288,6 +1463,14 @@ export async function deploySite(client, siteId, body) {
|
|
|
1288
1463
|
{ field: "AuthoringPreviewFailed", status: error.status },
|
|
1289
1464
|
);
|
|
1290
1465
|
}
|
|
1466
|
+
if (error instanceof ApiError && error.hasField("ExpectedPageCount")) {
|
|
1467
|
+
throw new SiteAuthoringError(
|
|
1468
|
+
"deploy.page_estimate_changed",
|
|
1469
|
+
`${error.descriptionFor("ExpectedPageCount") ?? "The pages this publish generates have grown since the estimate."} `
|
|
1470
|
+
+ "Nothing was staged; run the command again to confirm the new count.",
|
|
1471
|
+
{ field: "ExpectedPageCount", status: error.status },
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1291
1474
|
throw error;
|
|
1292
1475
|
}
|
|
1293
1476
|
const deployment = normalizeDeployment(response.deployment);
|