@taprootio/site-authoring 0.14.0 → 0.15.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 +62 -10
- package/examples/riverbend-wellness/.taproot-site-media.json +23 -0
- package/examples/riverbend-wellness/pages/index.md +1 -17
- package/package.json +2 -2
- package/src/api.js +59 -1
- package/src/cli.js +147 -22
- package/src/constants.js +6 -0
- package/src/content/components.js +64 -6
- package/src/content/free-form-sections.js +21 -4
- package/src/content/markdown.js +161 -26
- package/src/content/tiptap-prosemirror.ts +66 -8
- package/src/content/validate-document.js +30 -6
- package/src/content/vocabulary.js +26 -0
- package/src/errors.js +22 -0
- package/src/espalier-shared.js +11 -0
- package/src/font-catalog.js +87 -0
- package/src/footer-contract.js +1 -1
- package/src/output.js +19 -1
- package/src/presentation-reference.js +21 -7
- package/src/problems.js +116 -0
- package/src/reference-help.js +100 -34
- package/src/session.js +13 -0
- package/src/theme-proposals.js +393 -0
- package/src/theme-validation.js +31 -10
- package/src/typed-page-reference.js +11 -3
- package/src/typed-pages.js +31 -9
- package/src/verbs/deploy.js +1 -0
- package/src/verbs/index.js +13 -0
- package/src/verbs/media-upload.js +127 -30
- package/src/verbs/nav-push.js +7 -0
- package/src/verbs/pages-push.js +406 -199
- package/src/verbs/places.js +61 -0
- package/src/verbs/plan.js +585 -0
- package/src/verbs/theme-push.js +68 -36
- package/src/verbs/validate.js +264 -217
- package/src/video-prepare.js +48 -12
- package/src/workspace-check.js +199 -0
- package/src/workspace-ledger.js +173 -0
- package/src/workspace.js +47 -4
package/README.md
CHANGED
|
@@ -119,8 +119,16 @@ dark logo on the rendered dark header at desktop and compact/mobile widths;
|
|
|
119
119
|
transparent canvas pixels do not count as contrast. Supply separate
|
|
120
120
|
scheme-specific assets when one visible mark cannot satisfy both surfaces.
|
|
121
121
|
|
|
122
|
+
`taproot-site validate` checks the pulled workspace `taproot-site.json` names,
|
|
123
|
+
with no credential, no network, and no write: every page source (metadata,
|
|
124
|
+
Markdown conversion, media references, content vocabulary, section contexts),
|
|
125
|
+
page paths and templates against the pages the pull recorded, navigation,
|
|
126
|
+
redirects, forms, and the complete theme, appearance, header, brand, and
|
|
127
|
+
footer. It reports every problem at once in `error.problems`, each with its
|
|
128
|
+
file, code, and field, instead of one per run.
|
|
129
|
+
|
|
122
130
|
`taproot-site validate <fixture-directory>` checks a complete offline fixture
|
|
123
|
-
|
|
131
|
+
the same way: a directory laid out like a
|
|
124
132
|
pulled workspace — `pages/`, `nav.json`, `redirects.json`, and the four
|
|
125
133
|
`settings/` documents —
|
|
126
134
|
whose `manifest.fixture.json` binds deterministic page, resource, image, and
|
|
@@ -143,9 +151,10 @@ taproot-site validate "$(npm root --global)/@taprootio/site-authoring/examples/r
|
|
|
143
151
|
The fixture is a fictional wellness studio — `example.test` hostnames, a
|
|
144
152
|
reserved `555-01xx` telephone number, an invented street and town — with two
|
|
145
153
|
free-form pages and one each of the article, recipe, album and place-review
|
|
146
|
-
templates, a three-item navigation tree,
|
|
147
|
-
|
|
148
|
-
it
|
|
154
|
+
templates, a three-item navigation tree, all four settings documents, and a
|
|
155
|
+
`.taproot-site-media.json` that maps its one media path to a fixture image id.
|
|
156
|
+
Nothing in it describes a real business. Copy the directory somewhere writable
|
|
157
|
+
before you edit it; `validate` never writes to the fixture it reads.
|
|
149
158
|
|
|
150
159
|
## Authorize
|
|
151
160
|
|
|
@@ -285,14 +294,15 @@ each step names the reference it relies on.
|
|
|
285
294
|
3. **Edit both schemes and the appearance.** `help theme` lists every field,
|
|
286
295
|
the role slots, how anchor references resolve and what wins when a pin and
|
|
287
296
|
a role disagree; `help appearance` covers the default scheme, header, logos
|
|
288
|
-
and favicon
|
|
297
|
+
and favicon; `help fonts` lists the font families, by category, that the
|
|
298
|
+
font fields can name.
|
|
289
299
|
4. **Prepare media locally.** Raster files only (PNG, JPEG, GIF, WebP),
|
|
290
300
|
relative to the workspace root; make the transparent logo, its `@2x` copy,
|
|
291
301
|
a dark-scheme logo when needed and a square favicon with your own tools,
|
|
292
302
|
upload them with `media upload`, then assign `lightLogoId`, `darkLogoId`
|
|
293
303
|
and `brand.faviconId` from the returned ids. There is no recolor, crop or
|
|
294
304
|
SVG upload command.
|
|
295
|
-
5. **Validate, preview, then push.** `validate`
|
|
305
|
+
5. **Validate, preview, then push.** `validate` checks the whole workspace offline;
|
|
296
306
|
`theme push --dry-run` reads the site and lists the JSON paths at which
|
|
297
307
|
each settings file differs from it, and whether the baseline `pull`
|
|
298
308
|
recorded is still current; `theme push` saves both themes, the appearance
|
|
@@ -351,12 +361,40 @@ it is created: a source for another template is refused with
|
|
|
351
361
|
template's fields and one worked example.
|
|
352
362
|
|
|
353
363
|
`pages push [page-path...]` validates and sends the selected pages, or every
|
|
354
|
-
workspace page when none is named; the homepage is addressed as `/`.
|
|
364
|
+
workspace page when none is named; the homepage is addressed as `/`. Every
|
|
365
|
+
selected page is checked before anything is sent, and a refusal lists every
|
|
366
|
+
problem in `error.problems`. `pages push --dry-run` runs the same checks
|
|
367
|
+
against the live site, lists which pages would be created, updated, or left
|
|
368
|
+
unchanged, and sends nothing. It fails
|
|
355
369
|
closed with `pages.push_conflict` when a page's stored-state revision has
|
|
356
370
|
moved since this workspace last reconciled with it, naming both revisions;
|
|
357
371
|
`pull` first, or push after a refused pull has shown you the site's version.
|
|
358
|
-
|
|
359
|
-
|
|
372
|
+
|
|
373
|
+
`places search <name and city>` asks Google Places for the place a review
|
|
374
|
+
names and reports predictions with a session token; `places select
|
|
375
|
+
<googlePlaceId> <sessionToken>` records the match as a Taproot place and
|
|
376
|
+
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:`.
|
|
378
|
+
|
|
379
|
+
`plan` checks the whole workspace against the live site — pages, the media
|
|
380
|
+
they reference and the images the settings name, the footer and the pages it
|
|
381
|
+
links, the theme, navigation — and orders what is left
|
|
382
|
+
to send: media upload, pages push, footer push (theme push refuses unsaved
|
|
383
|
+
footer content), theme push, nav push, each marked ready, blocked, or nothing
|
|
384
|
+
to do. It writes nothing and reports a `planHash`. `apply --plan <planHash>`
|
|
385
|
+
plans again, refuses with `apply.plan_stale` if the workspace or the site
|
|
386
|
+
moved, and otherwise runs the steps through the ordinary push verbs. While
|
|
387
|
+
they run, a workspace file read differently from the plan (or not read by it)
|
|
388
|
+
stops the step before it writes, naming the file, and so does a page revision
|
|
389
|
+
or the site's navigation that moved. Steps are
|
|
390
|
+
not atomic together: a failure reports in `error.completedWrites` which steps
|
|
391
|
+
completed, which failed, and which did not run, and the next `plan` shows
|
|
392
|
+
finished steps as nothing to do. Redirects, forms, approval, and deployment
|
|
393
|
+
stay separate commands.
|
|
394
|
+
`media upload [path...]` uploads raster files and records each one in
|
|
395
|
+
`.taproot-site-media.json`; Markdown then names an image by its path (such as
|
|
396
|
+
`media/photo.webp`) everywhere an image goes, from body images to component and
|
|
397
|
+
section image fields. MP4, MOV, and WebM files are uploaded as videos. Taproot does not
|
|
360
398
|
encode: the command reads each file with mediabunny, rewrites an H.264 and AAC
|
|
361
399
|
file that is not an MP4 with its index first as one (copying the tracks, never
|
|
362
400
|
re-encoding), declares its codecs, and prints the server's one-line refusal as
|
|
@@ -372,7 +410,9 @@ settings, and the closed footer document. `pull` writes each scheme's
|
|
|
372
410
|
complete effective theme — the stored theme resolved over the same defaults
|
|
373
411
|
every consumer renders — so a fresh workspace validates as pulled, and it
|
|
374
412
|
keeps only authored `semanticMappings` pins (listed in
|
|
375
|
-
`explicitMappingTokens`
|
|
413
|
+
`explicitMappingTokens` at the root; a pin inside a context needs no marker)
|
|
414
|
+
because a pinned token shadows its role. Contrast and status-color warnings
|
|
415
|
+
name the nearest passing value the CLI found and checked.
|
|
376
416
|
`theme push` — and `theme push --dry-run`, which writes nothing — also runs
|
|
377
417
|
Espalier's fit report over the root and every context in both schemes and
|
|
378
418
|
prints each lint as a warning, such as a filled action that renders the
|
|
@@ -505,6 +545,18 @@ Other C0/C1 controls, U+2028/U+2029, U+FEFF, and literal `<br>` tags are rejecte
|
|
|
505
545
|
with the exact fact field path. A label is optional and stays outside the value's link. See
|
|
506
546
|
`taproot-site help page free-form` and `taproot-site help component cta`.
|
|
507
547
|
|
|
548
|
+
### Link rel in 0.15.0
|
|
549
|
+
|
|
550
|
+
A link publishes followed unless its author chooses otherwise. A link mark's
|
|
551
|
+
`rel` takes space-separated link types (`nofollow`, `sponsored`, `ugc`, `me`,
|
|
552
|
+
and the other standard types except `opener`), on any link; `target: "_blank"`
|
|
553
|
+
opens a new tab with `noopener noreferrer` added. In Markdown, write
|
|
554
|
+
`[Shop](https://shop.example){rel="sponsored nofollow" target="_blank"}`
|
|
555
|
+
(either attribute alone works). Other tokens and
|
|
556
|
+
targets are refused (`content.link_rel`, `content.link_target`). The editor's
|
|
557
|
+
old default, `noopener noreferrer nofollow`, reads as no choice. See
|
|
558
|
+
`taproot-site help page free-form`.
|
|
559
|
+
|
|
508
560
|
### Staging verification and deployment evidence
|
|
509
561
|
|
|
510
562
|
`deploy --staging` selects the same changed pages, settings groups and navigation
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mediaManifestVersion": 2,
|
|
3
|
+
"siteId": "a0000000-0000-4000-8000-000000000001",
|
|
4
|
+
"media": {
|
|
5
|
+
"media/riverbend-studio.webp": {
|
|
6
|
+
"imageId": "a0000000-0000-4000-8000-0000000000a2",
|
|
7
|
+
"src": "https://static.example.test/site/img/riverbend-studio-1280.webp",
|
|
8
|
+
"urls": [
|
|
9
|
+
{
|
|
10
|
+
"minWidth": 640,
|
|
11
|
+
"url": "https://static.example.test/site/img/riverbend-studio-640.webp"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"minWidth": 1280,
|
|
15
|
+
"url": "https://static.example.test/site/img/riverbend-studio-1280.webp"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"width": 1600,
|
|
19
|
+
"height": 1067,
|
|
20
|
+
"alt": "An example photograph placeholder for the fixture studio"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -19,23 +19,7 @@ description: An example home page for the offline authoring fixture.
|
|
|
19
19
|
"url": "https://schedule.example.test/riverbend"
|
|
20
20
|
},
|
|
21
21
|
"alignment": "start",
|
|
22
|
-
"media":
|
|
23
|
-
"imageId": "a0000000-0000-4000-8000-0000000000a2",
|
|
24
|
-
"src": "https://static.example.test/site/img/riverbend-studio-1280.webp",
|
|
25
|
-
"urls": [
|
|
26
|
-
{
|
|
27
|
-
"minWidth": 640,
|
|
28
|
-
"url": "https://static.example.test/site/img/riverbend-studio-640.webp"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"minWidth": 1280,
|
|
32
|
-
"url": "https://static.example.test/site/img/riverbend-studio-1280.webp"
|
|
33
|
-
}
|
|
34
|
-
],
|
|
35
|
-
"width": 1600,
|
|
36
|
-
"height": 1067,
|
|
37
|
-
"alt": "An example photograph placeholder for the fixture studio"
|
|
38
|
-
},
|
|
22
|
+
"media": "media/riverbend-studio.webp",
|
|
39
23
|
"mediaArrangement": "split",
|
|
40
24
|
"mediaPosition": "after",
|
|
41
25
|
"mediaWidth": "equal"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@taprootio/espalier": "
|
|
31
|
+
"@taprootio/espalier": "6.2.0",
|
|
32
32
|
"mediabunny": "1.61.1"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
package/src/api.js
CHANGED
|
@@ -178,7 +178,8 @@ const AUTHORING_PREVIEW_FIELD_ERRORS = Object.freeze({
|
|
|
178
178
|
],
|
|
179
179
|
[AUTHORING_PREVIEW_FIELDS.staging]: [
|
|
180
180
|
"preview.staging_unavailable",
|
|
181
|
-
"The site
|
|
181
|
+
"The site's staging address is not ready for previews. If the site has never deployed to staging, run "
|
|
182
|
+
+ "'deploy --staging' once; otherwise check its staging host with 'taproot-site status'.",
|
|
182
183
|
],
|
|
183
184
|
[AUTHORING_PREVIEW_FIELDS.expiry]: ["preview.expired", "The authoring preview has expired."],
|
|
184
185
|
[AUTHORING_PREVIEW_FIELDS.readiness]: ["preview.not_ready", "The authoring preview is not ready for a handoff."],
|
|
@@ -233,6 +234,11 @@ function requireIdentifier(value, code, field) {
|
|
|
233
234
|
return value;
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
/** A Google place id as Taproot stores it: the server's characters and its 256-character column. */
|
|
238
|
+
export function isGooglePlaceId(value) {
|
|
239
|
+
return typeof value === "string" && value.length <= 256 && /^[A-Za-z0-9_-]+$/u.test(value);
|
|
240
|
+
}
|
|
241
|
+
|
|
236
242
|
function requireCanonicalUuid(value, code, field) {
|
|
237
243
|
if (!isCanonicalUuid(value)) {
|
|
238
244
|
throw new SiteAuthoringError(code, `Taproot returned an invalid ${field}.`, { field });
|
|
@@ -874,6 +880,57 @@ export async function listVideos(client, siteId, requestOptions = {}) {
|
|
|
874
880
|
return { videos, truncated: true };
|
|
875
881
|
}
|
|
876
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Google Places autocomplete for a place review (TR01003). A key searches for
|
|
885
|
+
* the site it was issued for; every search and the select that follows share
|
|
886
|
+
* `sessionToken`, which is what groups them into one billed session.
|
|
887
|
+
*/
|
|
888
|
+
export async function searchPlaces(client, siteId, { text: searchText, sessionToken }) {
|
|
889
|
+
const search = query([["query", searchText], ["siteId", siteId], ["sessionToken", sessionToken]]);
|
|
890
|
+
const response = requireObject(
|
|
891
|
+
await client.request(`v1/places/search${search}`),
|
|
892
|
+
"api.place_contract",
|
|
893
|
+
"place search",
|
|
894
|
+
);
|
|
895
|
+
return (Array.isArray(response.predictions) ? response.predictions : []).map((value) => {
|
|
896
|
+
const prediction = requireObject(value, "api.place_contract", "place prediction");
|
|
897
|
+
return {
|
|
898
|
+
googlePlaceId: requireGooglePlaceId(prediction.googlePlaceId),
|
|
899
|
+
name: text(prediction.name),
|
|
900
|
+
formattedAddress: text(prediction.formattedAddress),
|
|
901
|
+
};
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function requireGooglePlaceId(value) {
|
|
906
|
+
if (!isGooglePlaceId(value)) {
|
|
907
|
+
throw new SiteAuthoringError("api.place_contract", "Taproot returned an invalid googlePlaceId.", { field: "googlePlaceId" });
|
|
908
|
+
}
|
|
909
|
+
return value;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** Records a prediction as a Taproot place, ending the search's billed session, and returns its id. */
|
|
913
|
+
export async function selectPlace(client, siteId, { googlePlaceId, sessionToken }) {
|
|
914
|
+
const place = requireObject(
|
|
915
|
+
await client.request("v1/places/select", {
|
|
916
|
+
method: "POST",
|
|
917
|
+
body: { siteId, googlePlaceId, sessionToken },
|
|
918
|
+
}),
|
|
919
|
+
"api.place_contract",
|
|
920
|
+
"selected place",
|
|
921
|
+
);
|
|
922
|
+
return {
|
|
923
|
+
// Any UUID version: the server mints place ids.
|
|
924
|
+
placeId: requireIdentifier(place.id, "api.place_contract", "id"),
|
|
925
|
+
name: text(place.name),
|
|
926
|
+
category: text(place.category),
|
|
927
|
+
address: text(place.address),
|
|
928
|
+
city: text(place.city),
|
|
929
|
+
region: text(place.region),
|
|
930
|
+
country: text(place.country),
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
877
934
|
/**
|
|
878
935
|
* Processing is observed through `ListSiteImages`; `GetImageById` is
|
|
879
936
|
* session-only under TR00602's read list, so a key-authorized client watches
|
|
@@ -1008,6 +1065,7 @@ export async function getPublishingReadiness(client, siteId, selection = {}) {
|
|
|
1008
1065
|
["stagedPageIds", selection.stagedPageIds],
|
|
1009
1066
|
["selectedSettingsTypes", selection.selectedSettingsTypes],
|
|
1010
1067
|
["includeNavigation", selection.includeNavigation],
|
|
1068
|
+
["useExactPageSelection", selection.useExactPageSelection],
|
|
1011
1069
|
])
|
|
1012
1070
|
}`,
|
|
1013
1071
|
)),
|
package/src/cli.js
CHANGED
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
DEPLOY_TARGET_STAGING,
|
|
11
11
|
LIMITS,
|
|
12
12
|
LOGIN_KEY_NAME_MAXIMUM,
|
|
13
|
+
PLAN_HASH,
|
|
13
14
|
PUBLISH_KEY_ENVIRONMENT_VARIABLE,
|
|
14
15
|
RESULT_SCHEMA_VERSION,
|
|
15
16
|
SURFACE_DOCS_PRESENTATION,
|
|
16
17
|
SURFACE_STANDARD,
|
|
18
|
+
VERB_APPLY,
|
|
17
19
|
VERB_APPROVE,
|
|
18
20
|
VERB_DELIVERY_CHECK,
|
|
19
21
|
VERB_DEPLOY,
|
|
@@ -29,6 +31,9 @@ import {
|
|
|
29
31
|
VERB_MEDIA_UPLOAD,
|
|
30
32
|
VERB_NAV_PUSH,
|
|
31
33
|
VERB_PAGES_PUSH,
|
|
34
|
+
VERB_PLACES_SEARCH,
|
|
35
|
+
VERB_PLACES_SELECT,
|
|
36
|
+
VERB_PLAN,
|
|
32
37
|
VERB_PREVIEW_PAGE,
|
|
33
38
|
VERB_PREVIEW_REVOKE,
|
|
34
39
|
VERB_PULL,
|
|
@@ -68,6 +73,7 @@ import {
|
|
|
68
73
|
REFERENCE_TOPICS,
|
|
69
74
|
REFERENCE_VERSION,
|
|
70
75
|
} from "./reference-help.js";
|
|
76
|
+
import { FONT_CATEGORIES, getFontCatalogReference } from "./font-catalog.js";
|
|
71
77
|
import { assertCliCurrent } from "./session.js";
|
|
72
78
|
import { VERB_HANDLERS } from "./verbs/index.js";
|
|
73
79
|
|
|
@@ -95,15 +101,20 @@ const VERBS = Object.freeze([
|
|
|
95
101
|
{
|
|
96
102
|
name: VERB_VALIDATE,
|
|
97
103
|
tokens: ["validate"],
|
|
98
|
-
summary: "
|
|
104
|
+
summary: "Check a whole workspace or offline fixture at once, or initialize a fixture with --init.",
|
|
99
105
|
positionals: "fixturePath",
|
|
100
106
|
offline: true,
|
|
101
107
|
note:
|
|
102
|
-
"
|
|
103
|
-
+ "
|
|
104
|
-
+ "
|
|
105
|
-
+
|
|
106
|
-
+ "
|
|
108
|
+
"With no directory, checks the pulled workspace taproot-site.json names; with one, checks that directory, which "
|
|
109
|
+
+ "is either an offline fixture (manifest.fixture.json) or a pulled workspace. Every page source, the theme, "
|
|
110
|
+
+ "appearance, header, brand, footer, navigation, redirects, and forms are checked with the validators the "
|
|
111
|
+
+ "pushes use, and every problem is reported at once in error.problems (each with its file, code, and field) "
|
|
112
|
+
+ "rather than one per run. A pulled workspace is checked against the pages, images, and baselines its "
|
|
113
|
+
+ "manifests recorded at the pull. This proves local structure and semantics only. It does not prove "
|
|
114
|
+
+ "authorization, live site ownership, concurrency, persisted round trips, or rendering: 'plan' checks a "
|
|
115
|
+
+ "workspace against the live site, and 'pages push --dry-run' does that for pages alone. "
|
|
116
|
+
+ `See '${CLI_BINARY_NAME} help fixture' for the fixture manifest contract and for the path of the complete `
|
|
117
|
+
+ "example fixture this package ships, which needs no credential and no pulled site. "
|
|
107
118
|
+ "With --init, the directory is a new destination; the current pulled workspace or --config supplies the source.",
|
|
108
119
|
},
|
|
109
120
|
{
|
|
@@ -224,6 +235,7 @@ const VERBS = Object.freeze([
|
|
|
224
235
|
surface: SURFACE_STANDARD,
|
|
225
236
|
capabilities: [CAPABILITY_CONTENT],
|
|
226
237
|
tokens: ["pages", "push"],
|
|
238
|
+
dryRun: true,
|
|
227
239
|
summary: "Create and update pages from the local workspace.",
|
|
228
240
|
positionals: "pagePaths",
|
|
229
241
|
note:
|
|
@@ -237,8 +249,71 @@ const VERBS = Object.freeze([
|
|
|
237
249
|
+ "documents of pages it is not sending: an unrelated page left on an obsolete contract is reported by the "
|
|
238
250
|
+ "whole-workspace push, not used to block this one. The result states the selection and how many sources were "
|
|
239
251
|
+ "discovered and validated. "
|
|
252
|
+
+ "Every selected page is checked before anything is sent, and a refusal lists every problem found in "
|
|
253
|
+
+ "error.problems with its file, code, and field. --dry-run does all of that against the live site, reports "
|
|
254
|
+
+ "which pages would be created, updated, or left unchanged, and sends nothing; it exits 1 when there are "
|
|
255
|
+
+ "problems. "
|
|
240
256
|
+ "See 'taproot-site help page free-form' for the stable manifest and error contract.",
|
|
241
257
|
},
|
|
258
|
+
{
|
|
259
|
+
name: VERB_PLACES_SEARCH,
|
|
260
|
+
surface: SURFACE_STANDARD,
|
|
261
|
+
// A key finds the places for the place reviews it writes, so page editing
|
|
262
|
+
// is what it needs; each search is a billed Google Places request.
|
|
263
|
+
capabilities: [CAPABILITY_CONTENT],
|
|
264
|
+
tokens: ["places", "search"],
|
|
265
|
+
positionals: "placeQuery",
|
|
266
|
+
summary: "Search Google Places for the place a review names; reports predictions and a session token.",
|
|
267
|
+
note: "Give the place's name and city, such as 'places search Blue Bottle Coffee Oakland'. Each prediction "
|
|
268
|
+
+ "carries a googlePlaceId; pass the one that matches, with the reported sessionToken, to 'places select' to "
|
|
269
|
+
+ "get the Taproot placeId a place review's front matter needs. Searches and the select that follows them are "
|
|
270
|
+
+ "one billed session, so search with a precise query rather than many broad ones.",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: VERB_PLACES_SELECT,
|
|
274
|
+
surface: SURFACE_STANDARD,
|
|
275
|
+
capabilities: [CAPABILITY_CONTENT],
|
|
276
|
+
tokens: ["places", "select"],
|
|
277
|
+
positionals: "placeSelection",
|
|
278
|
+
summary: "Record a places search prediction as a Taproot place and report its placeId.",
|
|
279
|
+
note: "Takes the googlePlaceId from 'places search', then that search's sessionToken. The place is recorded "
|
|
280
|
+
+ "once for every site, so selecting it again returns the same placeId. Use it as placeId in a place review, "
|
|
281
|
+
+ "or as place in an article or album.",
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: VERB_PLAN,
|
|
285
|
+
surface: SURFACE_STANDARD,
|
|
286
|
+
// Pages, media and navigation reads are Content; the presentation read and
|
|
287
|
+
// the footer draft are Design. The same set apply writes with.
|
|
288
|
+
capabilities: [CAPABILITY_CONTENT, CAPABILITY_DESIGN],
|
|
289
|
+
tokens: ["plan"],
|
|
290
|
+
summary: "Check the whole workspace against the live site and order what is left to send. Writes nothing.",
|
|
291
|
+
note: "Runs every check the pushes run — each page against the site's pages, revisions and videos; media that "
|
|
292
|
+
+ "pages reference and media upload has not sent; the theme, appearance and footer colors against the site's "
|
|
293
|
+
+ "presentation revision; the footer against its draft; navigation against the live page list — and lists "
|
|
294
|
+
+ "every problem with its area, file, code and field. It then orders the steps that remain: media upload, "
|
|
295
|
+
+ "pages push, footer push (theme push refuses unsaved footer content), theme push, nav push, each marked "
|
|
296
|
+
+ "ready, blocked, or nothing to do with the reason for its place. The result's planHash covers what each "
|
|
297
|
+
+ "step would send and the live state it would meet; pass it to 'apply'. Redirects and forms are not "
|
|
298
|
+
+ "covered: push them on their own. Approval and deployment stay separate.",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: VERB_APPLY,
|
|
302
|
+
surface: SURFACE_STANDARD,
|
|
303
|
+
capabilities: [CAPABILITY_CONTENT, CAPABILITY_DESIGN],
|
|
304
|
+
tokens: ["apply"],
|
|
305
|
+
planHash: true,
|
|
306
|
+
summary: "Run the steps a plan listed, in order, through the ordinary push verbs.",
|
|
307
|
+
note: "Plans again first and refuses with apply.plan_stale when the workspace or the site moved since the plan "
|
|
308
|
+
+ "you read, and with the plan's problems when it has any; nothing is written either way. While the steps "
|
|
309
|
+
+ "run, any workspace file read differently from the plan, or not read by the plan at all, stops the step "
|
|
310
|
+
+ "before it writes (apply.plan_stale, naming the file); so does a page revision or the site's navigation "
|
|
311
|
+
+ "that moved, and the theme and footer saves are refused by the site when it moved. Steps are not "
|
|
312
|
+
+ "atomic together: when one fails, the error's completedWrites lists the steps that completed, the one that "
|
|
313
|
+
+ "failed, and those that did not run. Run 'plan' again — finished steps read as nothing to do, and a step "
|
|
314
|
+
+ "whose answer was lost is reconciled by its own verb, never repeated blindly. Pages land as drafts; "
|
|
315
|
+
+ "'approve' and 'deploy' remain separate.",
|
|
316
|
+
},
|
|
242
317
|
{
|
|
243
318
|
name: VERB_NAV_PUSH,
|
|
244
319
|
surface: SURFACE_STANDARD,
|
|
@@ -344,8 +419,9 @@ const VERBS = Object.freeze([
|
|
|
344
419
|
+ "as a warning naming 'fit lint <id>' and does not stop the push, so read them in a --dry-run first. "
|
|
345
420
|
+ "pull writes each scheme's complete effective theme — the stored theme resolved over the same defaults every "
|
|
346
421
|
+ "consumer renders — so a fresh workspace validates as pulled. semanticMappings holds only authored pins, listed "
|
|
347
|
-
+ "in explicitMappingTokens; every other token compiles from roles
|
|
348
|
-
+ "mappings into a theme: a pinned token shadows its role."
|
|
422
|
+
+ "in explicitMappingTokens at the root (a context's pins need no marker); every other token compiles from roles "
|
|
423
|
+
+ "at render time, so never copy default mappings into a theme: a pinned token shadows its role. Contrast and "
|
|
424
|
+
+ "status-color warnings name the nearest passing value the CLI found and checked.",
|
|
349
425
|
},
|
|
350
426
|
{
|
|
351
427
|
name: VERB_FOOTER_PUSH,
|
|
@@ -591,7 +667,7 @@ ${COMMON_OPTIONS}
|
|
|
591
667
|
function verbHelp(verb) {
|
|
592
668
|
const targetUsage = verb.target ? " (--staging | --production)" : "";
|
|
593
669
|
const positionalUsage = verb.positionals === "fixturePath"
|
|
594
|
-
? " <
|
|
670
|
+
? " [<directory>]"
|
|
595
671
|
: verb.positionals === "pageSelector"
|
|
596
672
|
? " <page-path-or-id>"
|
|
597
673
|
: verb.positionals === "previewIds"
|
|
@@ -602,6 +678,10 @@ function verbHelp(verb) {
|
|
|
602
678
|
? " <site-name-or-id>"
|
|
603
679
|
: verb.positionals === "formKeys"
|
|
604
680
|
? " [form-key...]"
|
|
681
|
+
: verb.positionals === "placeQuery"
|
|
682
|
+
? " <query...>"
|
|
683
|
+
: verb.positionals === "placeSelection"
|
|
684
|
+
? " <google-place-id> <session-token>"
|
|
605
685
|
: verb.positionals
|
|
606
686
|
? ` [${verb.positionals === "paths" ? "path" : "page-path"}...]`
|
|
607
687
|
: "";
|
|
@@ -621,6 +701,9 @@ function verbHelp(verb) {
|
|
|
621
701
|
const dryRunOption = verb.dryRun
|
|
622
702
|
? "\n --dry-run Read the site and report what a push would change, without writing."
|
|
623
703
|
: "";
|
|
704
|
+
const planOption = verb.planHash
|
|
705
|
+
? "\n --plan <hash> The planHash 'plan' reported; required."
|
|
706
|
+
: "";
|
|
624
707
|
const nameOption = verb.keyName
|
|
625
708
|
? `\n --name <text> Name recorded on the issued key (default "${DEFAULT_LOGIN_KEY_NAME}",\n`
|
|
626
709
|
+ ` 1-${LOGIN_KEY_NAME_MAXIMUM} characters). The approval screen shows it.`
|
|
@@ -644,8 +727,9 @@ function verbHelp(verb) {
|
|
|
644
727
|
+ "Taproot accepts only the latest; with nothing recorded it runs."
|
|
645
728
|
: "";
|
|
646
729
|
const boundary = verb.name === VERB_VALIDATE
|
|
647
|
-
? "Validation uses no credential,
|
|
648
|
-
+
|
|
730
|
+
? "Validation uses no credential, network or write; it reads the configuration only to find the workspace when "
|
|
731
|
+
+ "no directory is given. With --init it reads the pulled workspace or selected configuration and writes a new "
|
|
732
|
+
+ `fixture directory, still without credentials or network.${upgradeGate}`
|
|
649
733
|
: selfContained
|
|
650
734
|
? "This offline verb uses no credential and reads no configuration, and performs no network request and no "
|
|
651
735
|
+ `write.${upgradeGate}`
|
|
@@ -661,7 +745,8 @@ function verbHelp(verb) {
|
|
|
661
745
|
const options = verb.name === VERB_VALIDATE
|
|
662
746
|
? `Options:
|
|
663
747
|
--init Export a pulled workspace into the new fixture directory.
|
|
664
|
-
--config <path> Before the verb;
|
|
748
|
+
--config <path> Before the verb; the configuration naming the workspace (no
|
|
749
|
+
directory) or the --init source.
|
|
665
750
|
--quiet Suppress human progress. The JSON result is unchanged.
|
|
666
751
|
--help Show this help.
|
|
667
752
|
--version Show the package version.`
|
|
@@ -688,7 +773,7 @@ function verbHelp(verb) {
|
|
|
688
773
|
${verb.summary}
|
|
689
774
|
${boundary}
|
|
690
775
|
|
|
691
|
-
${options}${targetOption}${deliveryTargetOption}${jsonOption}${dryRunOption}${nameOption}${note}
|
|
776
|
+
${options}${targetOption}${deliveryTargetOption}${jsonOption}${dryRunOption}${planOption}${nameOption}${note}
|
|
692
777
|
`;
|
|
693
778
|
}
|
|
694
779
|
|
|
@@ -761,6 +846,9 @@ function parseReferenceArguments(arguments_) {
|
|
|
761
846
|
) {
|
|
762
847
|
throw usageError("help.usage", `The '${topic}' topic does not accept a name.`);
|
|
763
848
|
}
|
|
849
|
+
if (topic === "fonts" && extra.length > 0) {
|
|
850
|
+
throw usageError("help.usage", "The 'fonts' topic accepts at most one category.");
|
|
851
|
+
}
|
|
764
852
|
if ((topic === "page" || topic === "component" || topic === "design") && (subject === undefined || extra.length > 0)) {
|
|
765
853
|
throw usageError("help.usage", `The '${topic}' topic requires exactly one name.`);
|
|
766
854
|
}
|
|
@@ -833,6 +921,17 @@ function referenceResult(parsed) {
|
|
|
833
921
|
referenceKind: parsed.topic,
|
|
834
922
|
reference: getWorkflowReference(parsed.topic),
|
|
835
923
|
};
|
|
924
|
+
case "fonts": {
|
|
925
|
+
const fonts = getFontCatalogReference(parsed.subject);
|
|
926
|
+
if (!fonts) {
|
|
927
|
+
throw usageError(
|
|
928
|
+
"help.font_category_unknown",
|
|
929
|
+
`Unknown font category. Expected one of: ${FONT_CATEGORIES.join(", ")}.`,
|
|
930
|
+
{ alternatives: FONT_CATEGORIES },
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
return { ...result, topic: "fonts", fonts };
|
|
934
|
+
}
|
|
836
935
|
case "theme":
|
|
837
936
|
return { ...result, topic: "presentation", referenceKind: "theme", reference: getThemeReference() };
|
|
838
937
|
case "appearance":
|
|
@@ -896,6 +995,16 @@ function parseWaitOption(arguments_, index) {
|
|
|
896
995
|
return seconds;
|
|
897
996
|
}
|
|
898
997
|
|
|
998
|
+
function parsePlanOption(arguments_, index) {
|
|
999
|
+
const candidate = arguments_[index + 1];
|
|
1000
|
+
if (typeof candidate !== "string" || !PLAN_HASH.test(candidate)) {
|
|
1001
|
+
throw usageError("apply.plan_required", "--plan requires the planHash 'plan' reported (sha256:<64 hex>).", {
|
|
1002
|
+
field: "planHash",
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
return candidate;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
899
1008
|
function parseNameOption(arguments_, index) {
|
|
900
1009
|
const candidate = arguments_[index + 1];
|
|
901
1010
|
if (
|
|
@@ -948,10 +1057,11 @@ function parseArguments(arguments_) {
|
|
|
948
1057
|
// origin the sign-in belongs to — so they accept it. whoami is offline and
|
|
949
1058
|
// reads both the store and the configuration, so it accepts it too: the test
|
|
950
1059
|
// is "reads nothing local", not "makes no request".
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1060
|
+
// validate reads the configuration in two cases: --init, and no directory,
|
|
1061
|
+
// which checks the workspace the configuration names.
|
|
1062
|
+
const validateReadsConfig = verb.name === VERB_VALIDATE
|
|
1063
|
+
&& (rest.includes("--init") || rest.every((argument) => argument.startsWith("-")));
|
|
1064
|
+
if (verb.offline && !verb.readsLocalState && configPath !== undefined && !validateReadsConfig) {
|
|
955
1065
|
throw usageError("cli.config_option", "--config applies only to verbs that read the site configuration.", {
|
|
956
1066
|
field: "configPath",
|
|
957
1067
|
});
|
|
@@ -969,6 +1079,7 @@ function parseArguments(arguments_) {
|
|
|
969
1079
|
let dryRun = false;
|
|
970
1080
|
let json = false;
|
|
971
1081
|
let keyName;
|
|
1082
|
+
let planHash;
|
|
972
1083
|
const positionals = [];
|
|
973
1084
|
for (let index = 0; index < rest.length; index += 1) {
|
|
974
1085
|
const argument = rest[index];
|
|
@@ -1035,6 +1146,12 @@ function parseArguments(arguments_) {
|
|
|
1035
1146
|
json = true;
|
|
1036
1147
|
continue;
|
|
1037
1148
|
}
|
|
1149
|
+
if (verb.planHash && argument === "--plan") {
|
|
1150
|
+
if (planHash !== undefined) throw usageError("cli.duplicate_option", "--plan may be supplied only once.");
|
|
1151
|
+
planHash = parsePlanOption(rest, index);
|
|
1152
|
+
index += 1;
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1038
1155
|
if (verb.keyName && argument === "--name") {
|
|
1039
1156
|
if (keyName !== undefined) throw usageError("cli.duplicate_option", "--name may be supplied only once.");
|
|
1040
1157
|
keyName = parseNameOption(rest, index);
|
|
@@ -1066,13 +1183,10 @@ function parseArguments(arguments_) {
|
|
|
1066
1183
|
`${verb.tokens.join(" ")} requires exactly one of --staging or --production.`,
|
|
1067
1184
|
);
|
|
1068
1185
|
}
|
|
1069
|
-
if (
|
|
1070
|
-
verb.positionals === "fixturePath"
|
|
1071
|
-
&& positionals.length !== 1
|
|
1072
|
-
) {
|
|
1186
|
+
if (verb.positionals === "fixturePath" && (positionals.length > 1 || (init && positionals.length !== 1))) {
|
|
1073
1187
|
throw usageError(
|
|
1074
1188
|
"validate.fixture_path_invalid",
|
|
1075
|
-
"validate requires exactly one fixture directory.",
|
|
1189
|
+
init ? "validate --init requires exactly one new fixture directory." : "validate accepts at most one directory.",
|
|
1076
1190
|
{ field: "fixturePath" },
|
|
1077
1191
|
);
|
|
1078
1192
|
}
|
|
@@ -1120,6 +1234,11 @@ function parseArguments(arguments_) {
|
|
|
1120
1234
|
{ field: "environmentSelector" },
|
|
1121
1235
|
);
|
|
1122
1236
|
}
|
|
1237
|
+
if (verb.planHash && planHash === undefined) {
|
|
1238
|
+
throw usageError("apply.plan_required", "apply requires --plan with the planHash that 'plan' reported.", {
|
|
1239
|
+
field: "planHash",
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1123
1242
|
if (quiet && verb.name === VERB_LOGIN) {
|
|
1124
1243
|
// The approval URL and user code reach the operator only as progress
|
|
1125
1244
|
// lines, and the JSON result is serialized only after polling ends — so a
|
|
@@ -1161,6 +1280,7 @@ function parseArguments(arguments_) {
|
|
|
1161
1280
|
init,
|
|
1162
1281
|
dryRun,
|
|
1163
1282
|
keyName,
|
|
1283
|
+
...(planHash === undefined ? {} : { planHash }),
|
|
1164
1284
|
positionals: verb.positionals
|
|
1165
1285
|
? {
|
|
1166
1286
|
key: verb.positionals,
|
|
@@ -1244,6 +1364,11 @@ export async function runCli({
|
|
|
1244
1364
|
quiet: parsed.quiet,
|
|
1245
1365
|
dryRun: parsed.dryRun,
|
|
1246
1366
|
keyName: parsed.keyName,
|
|
1367
|
+
...(parsed.planHash === undefined ? {} : { planHash: parsed.planHash }),
|
|
1368
|
+
// Only when given, like the other optional fields, so each verb keeps its default (TR00925).
|
|
1369
|
+
...(parsed.deliveryUrl === undefined ? {} : { deliveryUrl: parsed.deliveryUrl }),
|
|
1370
|
+
...(parsed.propagationWaitSeconds === undefined ? {} : { propagationWaitSeconds: parsed.propagationWaitSeconds }),
|
|
1371
|
+
...(parsed.browser === undefined ? {} : { browser: parsed.browser }),
|
|
1247
1372
|
capabilities: parsed.capabilities,
|
|
1248
1373
|
surface: parsed.surface,
|
|
1249
1374
|
surfaceCapabilities: parsed.surfaceCapabilities,
|
package/src/constants.js
CHANGED
|
@@ -103,6 +103,12 @@ export const VERB_STAGING_REVIEW = "staging review";
|
|
|
103
103
|
export const VERB_SITES = "sites";
|
|
104
104
|
export const VERB_USE = "use";
|
|
105
105
|
export const VERB_WHOAMI = "whoami";
|
|
106
|
+
export const VERB_PLAN = "plan";
|
|
107
|
+
export const VERB_APPLY = "apply";
|
|
108
|
+
export const VERB_PLACES_SEARCH = "places search";
|
|
109
|
+
export const VERB_PLACES_SELECT = "places select";
|
|
110
|
+
/** The identity `plan` reports and `apply --plan` requires. */
|
|
111
|
+
export const PLAN_HASH = /^sha256:[0-9a-f]{64}$/u;
|
|
106
112
|
export const VERB_ENV = "env";
|
|
107
113
|
|
|
108
114
|
// Which authoring verbs a site accepts (TR00790), as `use` records it in the
|