@taprootio/site-authoring 0.14.1 → 0.16.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 +181 -16
- 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 +123 -1
- package/src/authors-contract.js +170 -0
- package/src/cli.js +355 -30
- package/src/constants.js +9 -0
- package/src/content/components.js +64 -6
- package/src/content/free-form-sections.js +21 -4
- package/src/content/link-warnings.js +109 -0
- 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/fixture-init.js +55 -3
- package/src/font-catalog.js +87 -0
- package/src/footer-contract.js +1 -1
- package/src/output.js +19 -1
- package/src/page-metadata.js +92 -0
- package/src/presentation-reference.js +21 -7
- package/src/problems.js +116 -0
- package/src/reference-help.js +157 -41
- package/src/session.js +13 -0
- package/src/staging-check.js +62 -0
- package/src/theme-proposals.js +393 -0
- package/src/theme-validation.js +31 -10
- package/src/typed-page-reference.js +97 -21
- package/src/typed-pages.js +196 -13
- package/src/verbs/approve.js +63 -14
- package/src/verbs/authors.js +167 -0
- package/src/verbs/deploy.js +5 -28
- package/src/verbs/index.js +22 -0
- package/src/verbs/media-upload.js +127 -30
- package/src/verbs/nav-push.js +7 -0
- package/src/verbs/pages-meta-set.js +303 -0
- package/src/verbs/pages-push.js +814 -224
- package/src/verbs/places.js +61 -0
- package/src/verbs/plan.js +595 -0
- package/src/verbs/pull.js +255 -20
- package/src/verbs/redirects-check.js +4 -10
- package/src/verbs/theme-push.js +68 -36
- package/src/verbs/validate.js +309 -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 +105 -10
package/src/cli.js
CHANGED
|
@@ -10,11 +10,15 @@ 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,
|
|
20
|
+
VERB_AUTHORS_ADD,
|
|
21
|
+
VERB_AUTHORS_LIST,
|
|
18
22
|
VERB_DELIVERY_CHECK,
|
|
19
23
|
VERB_DEPLOY,
|
|
20
24
|
VERB_STAGING_REVIEW,
|
|
@@ -28,7 +32,11 @@ import {
|
|
|
28
32
|
VERB_LOGOUT,
|
|
29
33
|
VERB_MEDIA_UPLOAD,
|
|
30
34
|
VERB_NAV_PUSH,
|
|
35
|
+
VERB_PAGES_META_SET,
|
|
31
36
|
VERB_PAGES_PUSH,
|
|
37
|
+
VERB_PLACES_SEARCH,
|
|
38
|
+
VERB_PLACES_SELECT,
|
|
39
|
+
VERB_PLAN,
|
|
32
40
|
VERB_PREVIEW_PAGE,
|
|
33
41
|
VERB_PREVIEW_REVOKE,
|
|
34
42
|
VERB_PULL,
|
|
@@ -68,6 +76,7 @@ import {
|
|
|
68
76
|
REFERENCE_TOPICS,
|
|
69
77
|
REFERENCE_VERSION,
|
|
70
78
|
} from "./reference-help.js";
|
|
79
|
+
import { FONT_CATEGORIES, getFontCatalogReference } from "./font-catalog.js";
|
|
71
80
|
import { assertCliCurrent } from "./session.js";
|
|
72
81
|
import { VERB_HANDLERS } from "./verbs/index.js";
|
|
73
82
|
|
|
@@ -95,15 +104,22 @@ const VERBS = Object.freeze([
|
|
|
95
104
|
{
|
|
96
105
|
name: VERB_VALIDATE,
|
|
97
106
|
tokens: ["validate"],
|
|
98
|
-
summary: "
|
|
107
|
+
summary: "Check a whole workspace or offline fixture at once, or initialize a fixture with --init.",
|
|
99
108
|
positionals: "fixturePath",
|
|
100
109
|
offline: true,
|
|
101
110
|
note:
|
|
102
|
-
"
|
|
103
|
-
+ "
|
|
104
|
-
+ "
|
|
105
|
-
+
|
|
106
|
-
+ "
|
|
111
|
+
"With no directory, checks the pulled workspace taproot-site.json names; with one, checks that directory, which "
|
|
112
|
+
+ "is either an offline fixture (manifest.fixture.json) or a pulled workspace. Every page source, the theme, "
|
|
113
|
+
+ "appearance, header, brand, footer, navigation, redirects, and forms are checked with the validators the "
|
|
114
|
+
+ "pushes use, and every problem is reported at once in error.problems (each with its file, code, and field) "
|
|
115
|
+
+ "rather than one per run. A pulled workspace is checked offline against the pages (generated ones included, "
|
|
116
|
+
+ "so navigation to a section or tag page resolves), images, and baselines its manifests recorded at the pull. "
|
|
117
|
+
+ "A link whose address is only http(s):// plus its own text (left by older editor autolinking) is reported in "
|
|
118
|
+
+ "linkWarnings without refusing; pages push reports the same for the pages it sends. This proves local structure and semantics only. It does not prove "
|
|
119
|
+
+ "authorization, live site ownership, concurrency, persisted round trips, or rendering: 'plan' checks a "
|
|
120
|
+
+ "workspace against the live site, and 'pages push --dry-run' does that for pages alone. "
|
|
121
|
+
+ `See '${CLI_BINARY_NAME} help fixture' for the fixture manifest contract and for the path of the complete `
|
|
122
|
+
+ "example fixture this package ships, which needs no credential and no pulled site. "
|
|
107
123
|
+ "With --init, the directory is a new destination; the current pulled workspace or --config supplies the source.",
|
|
108
124
|
},
|
|
109
125
|
{
|
|
@@ -217,17 +233,79 @@ const VERBS = Object.freeze([
|
|
|
217
233
|
+ "site's version under '.taproot-site-state/', and leaves you to either push the local source or delete it "
|
|
218
234
|
+ "and pull again. A locally edited '.pm.json' is kept rather than overwritten for the same reason. "
|
|
219
235
|
+ "First revisions adopted without a completed body comparison are counted in pages.revisionsRecordedWithoutBodyComparison "
|
|
220
|
-
+ "and announced on the human channel. New downloads and compared bodies are excluded."
|
|
236
|
+
+ "and announced on the human channel. New downloads and compared bodies are excluded. "
|
|
237
|
+
+ "A page Taproot generates (a tag, an archive, a place, a folder) is pulled as an update-only '.pm.json' "
|
|
238
|
+
+ "(template 'generated'): you edit its custom title, breadcrumb title, custom description and introduction, "
|
|
239
|
+
+ "and the manifest keeps the title and description the site reports.",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: VERB_PAGES_META_SET,
|
|
243
|
+
tokens: ["pages", "meta", "set"],
|
|
244
|
+
offline: true,
|
|
245
|
+
// Offline, but it reads taproot-site.json and edits the workspace manifest.
|
|
246
|
+
readsLocalState: true,
|
|
247
|
+
writesWorkspace: true,
|
|
248
|
+
metaOptions: true,
|
|
249
|
+
positionals: "metaPagePath",
|
|
250
|
+
summary: "Set a tracked page's title, description or author in the workspace; 'pages push' sends it.",
|
|
251
|
+
note: "Names one page by its path ('/' for the homepage) and sets --title, --description, --author, or any of "
|
|
252
|
+
+ "them, in the workspace's record of that page. A .pm.json page keeps its title, description and author "
|
|
253
|
+
+ "there, so this is the supported way to change them; a Markdown page keeps them in its front matter, which "
|
|
254
|
+
+ "is edited instead. --author names a site author by handle or a member by email address (see 'authors "
|
|
255
|
+
+ "list'); it only fills a page that has no author, and a page that already has a different one is refused "
|
|
256
|
+
+ "(pages.author_conflict). A name missing from authors.json is only warned about (authors.json can be older "
|
|
257
|
+
+ "than the site's list); 'pages push' checks it against the site. --author \"\" drops an author that has not "
|
|
258
|
+
+ "been sent; it never removes the site's. "
|
|
259
|
+
+ "A generated page keeps its own in its source (data.customTitle and data.customDescription), which this "
|
|
260
|
+
+ "edits; an empty --title or --description there clears the custom value so the page uses its generated one. "
|
|
261
|
+
+ "A description longer than 160 characters is warned about (search results cut it off) and one longer than "
|
|
262
|
+
+ "1000 is refused. Nothing is sent: run 'pages push', then 'approve'.",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: VERB_AUTHORS_LIST,
|
|
266
|
+
surface: SURFACE_STANDARD,
|
|
267
|
+
// Naming an author is part of writing a page, and the site gates both the
|
|
268
|
+
// list and the creation on the permission that creates pages (TR01196).
|
|
269
|
+
capabilities: [CAPABILITY_CONTENT],
|
|
270
|
+
tokens: ["authors", "list"],
|
|
271
|
+
summary: "List the site authors and the members a page can be credited to; refreshes authors.json.",
|
|
272
|
+
note: "A page's author is a site author without a Taproot account (named by handle, such as jane-doe) or a site "
|
|
273
|
+
+ "member who can create pages (named by email address). Pages are authorless unless a source names one, and "
|
|
274
|
+
+ "an author already on a page is never replaced. The result lists authors { handle, displayName, hasEmail } "
|
|
275
|
+
+ "and members { email, displayName }, and rewrites authors.json, which the offline checks ('validate', "
|
|
276
|
+
+ "'pages meta set') read to warn about a name it lacks. authors.json holds members' email addresses: keep it "
|
|
277
|
+
+ "out of a public repository. "
|
|
278
|
+
+ "A site author's own address is private and is never listed or written.",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: VERB_AUTHORS_ADD,
|
|
282
|
+
surface: SURFACE_STANDARD,
|
|
283
|
+
capabilities: [CAPABILITY_CONTENT],
|
|
284
|
+
tokens: ["authors", "add"],
|
|
285
|
+
positionals: "authorHandle",
|
|
286
|
+
authorOptions: true,
|
|
287
|
+
summary: "Create a site author, someone a page can be credited to who has no Taproot account.",
|
|
288
|
+
note: "Give the handle (lowercase letters, digits and single hyphens, at most 64 characters; it never changes) "
|
|
289
|
+
+ "and --name, the name readers see on the byline. --email is optional: it is kept private, never published, "
|
|
290
|
+
+ "and lets the person be linked to a Taproot account later. Then credit pages to the author with "
|
|
291
|
+
+ "'author: <handle>' in a page's front matter or 'pages meta set <path> --author <handle>'. A handle the site "
|
|
292
|
+
+ "already has under another name is refused (authors.handle_taken); under the same name it succeeds with "
|
|
293
|
+
+ "existing: true. authors.json is updated with the new author.",
|
|
221
294
|
},
|
|
222
295
|
{
|
|
223
296
|
name: VERB_PAGES_PUSH,
|
|
224
297
|
surface: SURFACE_STANDARD,
|
|
225
298
|
capabilities: [CAPABILITY_CONTENT],
|
|
226
299
|
tokens: ["pages", "push"],
|
|
300
|
+
dryRun: true,
|
|
227
301
|
summary: "Create and update pages from the local workspace.",
|
|
228
302
|
positionals: "pagePaths",
|
|
229
303
|
note:
|
|
230
304
|
"Positional page paths narrow the push to those pages; with none, every workspace page is validated and sent. "
|
|
305
|
+
+ "A link in a page being sent whose address is only http(s):// plus its own text is reported in linkWarnings "
|
|
306
|
+
+ "(content.link_autolinked) without refusing. "
|
|
307
|
+
+ "A page is created without an author unless its source names one ('author:' in the front matter, or 'pages "
|
|
308
|
+
+ "meta set --author'); the pages created that way are listed in pages.authorless with how to credit them. "
|
|
231
309
|
+ "The homepage is recorded with an empty path, so address it as '/'. "
|
|
232
310
|
+ "A selected path resolves to its one authoritative source from metadata alone, and that page is then validated "
|
|
233
311
|
+ "exactly as a whole push would validate it — site binding, manifest integrity, live create-or-update "
|
|
@@ -237,8 +315,75 @@ const VERBS = Object.freeze([
|
|
|
237
315
|
+ "documents of pages it is not sending: an unrelated page left on an obsolete contract is reported by the "
|
|
238
316
|
+ "whole-workspace push, not used to block this one. The result states the selection and how many sources were "
|
|
239
317
|
+ "discovered and validated. "
|
|
318
|
+
+ "Every selected page is checked before anything is sent, and a refusal lists every problem found in "
|
|
319
|
+
+ "error.problems with its file, code, and field. --dry-run does all of that against the live site, reports "
|
|
320
|
+
+ "which pages would be created, updated, or left unchanged, and sends nothing; it exits 1 when there are "
|
|
321
|
+
+ "problems. "
|
|
322
|
+
+ "A generated page's source is update-only: a push refuses to create one (pages.generated_create), move it "
|
|
323
|
+
+ "(pages.generated_move), or change what it is (pages.generated_identity). A generated page you did not edit "
|
|
324
|
+
+ "never blocks a whole-workspace push, and one the site no longer has is reported as pages.staleGeneratedSources "
|
|
325
|
+
+ "and skipped (naming its path refuses with pages.generated_create). "
|
|
240
326
|
+ "See 'taproot-site help page free-form' for the stable manifest and error contract.",
|
|
241
327
|
},
|
|
328
|
+
{
|
|
329
|
+
name: VERB_PLACES_SEARCH,
|
|
330
|
+
surface: SURFACE_STANDARD,
|
|
331
|
+
// A key finds the places for the place reviews it writes, so page editing
|
|
332
|
+
// is what it needs; each search is a billed Google Places request.
|
|
333
|
+
capabilities: [CAPABILITY_CONTENT],
|
|
334
|
+
tokens: ["places", "search"],
|
|
335
|
+
positionals: "placeQuery",
|
|
336
|
+
summary: "Search Google Places for the place a review names; reports predictions and a session token.",
|
|
337
|
+
note: "Give the place's name and city, such as 'places search Blue Bottle Coffee Oakland'. Each prediction "
|
|
338
|
+
+ "carries a googlePlaceId; pass the one that matches, with the reported sessionToken, to 'places select' to "
|
|
339
|
+
+ "get the Taproot placeId a place review's front matter needs. Searches and the select that follows them are "
|
|
340
|
+
+ "one billed session, so search with a precise query rather than many broad ones.",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: VERB_PLACES_SELECT,
|
|
344
|
+
surface: SURFACE_STANDARD,
|
|
345
|
+
capabilities: [CAPABILITY_CONTENT],
|
|
346
|
+
tokens: ["places", "select"],
|
|
347
|
+
positionals: "placeSelection",
|
|
348
|
+
summary: "Record a places search prediction as a Taproot place and report its placeId.",
|
|
349
|
+
note: "Takes the googlePlaceId from 'places search', then that search's sessionToken. The place is recorded "
|
|
350
|
+
+ "once for every site, so selecting it again returns the same placeId. Use it as placeId in a place review, "
|
|
351
|
+
+ "or as place in an article or album.",
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: VERB_PLAN,
|
|
355
|
+
surface: SURFACE_STANDARD,
|
|
356
|
+
// Pages, media and navigation reads are Content; the presentation read and
|
|
357
|
+
// the footer draft are Design. The same set apply writes with.
|
|
358
|
+
capabilities: [CAPABILITY_CONTENT, CAPABILITY_DESIGN],
|
|
359
|
+
tokens: ["plan"],
|
|
360
|
+
summary: "Check the whole workspace against the live site and order what is left to send. Writes nothing.",
|
|
361
|
+
note: "Runs every check the pushes run — each page against the site's pages, revisions and videos; media that "
|
|
362
|
+
+ "pages reference and media upload has not sent; the theme, appearance and footer colors against the site's "
|
|
363
|
+
+ "presentation revision; the footer against its draft; navigation against the live page list — and lists "
|
|
364
|
+
+ "every problem with its area, file, code and field. It then orders the steps that remain: media upload, "
|
|
365
|
+
+ "pages push, footer push (theme push refuses unsaved footer content), theme push, nav push, each marked "
|
|
366
|
+
+ "ready, blocked, or nothing to do with the reason for its place. The result's planHash covers what each "
|
|
367
|
+
+ "step would send and the live state it would meet; pass it to 'apply'. Redirects and forms are not "
|
|
368
|
+
+ "covered: push them on their own. Approval and deployment stay separate.",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: VERB_APPLY,
|
|
372
|
+
surface: SURFACE_STANDARD,
|
|
373
|
+
capabilities: [CAPABILITY_CONTENT, CAPABILITY_DESIGN],
|
|
374
|
+
tokens: ["apply"],
|
|
375
|
+
planHash: true,
|
|
376
|
+
summary: "Run the steps a plan listed, in order, through the ordinary push verbs.",
|
|
377
|
+
note: "Plans again first and refuses with apply.plan_stale when the workspace or the site moved since the plan "
|
|
378
|
+
+ "you read, and with the plan's problems when it has any; nothing is written either way. While the steps "
|
|
379
|
+
+ "run, any workspace file read differently from the plan, or not read by the plan at all, stops the step "
|
|
380
|
+
+ "before it writes (apply.plan_stale, naming the file); so does a page revision or the site's navigation "
|
|
381
|
+
+ "that moved, and the theme and footer saves are refused by the site when it moved. Steps are not "
|
|
382
|
+
+ "atomic together: when one fails, the error's completedWrites lists the steps that completed, the one that "
|
|
383
|
+
+ "failed, and those that did not run. Run 'plan' again — finished steps read as nothing to do, and a step "
|
|
384
|
+
+ "whose answer was lost is reconciled by its own verb, never repeated blindly. Pages land as drafts; "
|
|
385
|
+
+ "'approve' and 'deploy' remain separate.",
|
|
386
|
+
},
|
|
242
387
|
{
|
|
243
388
|
name: VERB_NAV_PUSH,
|
|
244
389
|
surface: SURFACE_STANDARD,
|
|
@@ -255,9 +400,9 @@ const VERBS = Object.freeze([
|
|
|
255
400
|
capabilities: [CAPABILITY_CONTENT, CAPABILITY_DEPLOYMENTS],
|
|
256
401
|
tokens: ["redirects", "check"],
|
|
257
402
|
summary: "Check the current redirect map through the authenticated staging edge.",
|
|
258
|
-
note: "Reports path, HTTP status and Location
|
|
259
|
-
+ "that carries it, so this reads the staged release. "
|
|
260
|
-
+ "The result includes a fresh single-use staging handoff; keep it private.",
|
|
403
|
+
note: "Reports each entry's path, HTTP status and Location in redirects.items, and redirects.verified, without "
|
|
404
|
+
+ "following redirects. A redirect is live only with the release that carries it, so this reads the staged release. "
|
|
405
|
+
+ "The result includes a fresh single-use staging handoff in stagingPreview.url; keep it private.",
|
|
261
406
|
},
|
|
262
407
|
{
|
|
263
408
|
name: VERB_REDIRECTS_PULL,
|
|
@@ -344,8 +489,9 @@ const VERBS = Object.freeze([
|
|
|
344
489
|
+ "as a warning naming 'fit lint <id>' and does not stop the push, so read them in a --dry-run first. "
|
|
345
490
|
+ "pull writes each scheme's complete effective theme — the stored theme resolved over the same defaults every "
|
|
346
491
|
+ "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."
|
|
492
|
+
+ "in explicitMappingTokens at the root (a context's pins need no marker); every other token compiles from roles "
|
|
493
|
+
+ "at render time, so never copy default mappings into a theme: a pinned token shadows its role. Contrast and "
|
|
494
|
+
+ "status-color warnings name the nearest passing value the CLI found and checked.",
|
|
349
495
|
},
|
|
350
496
|
{
|
|
351
497
|
name: VERB_FOOTER_PUSH,
|
|
@@ -381,7 +527,10 @@ const VERBS = Object.freeze([
|
|
|
381
527
|
positionals: "pagePaths",
|
|
382
528
|
note: "Positional arguments narrow the selection to those page paths;"
|
|
383
529
|
+ " with none, every draft the workspace manifest tracks is staged."
|
|
384
|
-
+ " The homepage is recorded with an empty path, so address it as '/'."
|
|
530
|
+
+ " The homepage is recorded with an empty path, so address it as '/'."
|
|
531
|
+
+ " A named page with no pending draft (already approved, or published with no new draft) is listed in"
|
|
532
|
+
+ " skipped { total, items: [{ pageId, path, status, reason }] } and the rest are approved; a path that names"
|
|
533
|
+
+ " no page this workspace tracks refuses the whole call.",
|
|
385
534
|
},
|
|
386
535
|
{
|
|
387
536
|
name: VERB_DEPLOY,
|
|
@@ -403,7 +552,8 @@ const VERBS = Object.freeze([
|
|
|
403
552
|
note: "deploy --staging returns a single-use review handoff in stagingPreview.url, on a managed Docs site too, "
|
|
404
553
|
+ "where it stages settings only: open the URL once, then switch the site's theme toggle to review both "
|
|
405
554
|
+ "schemes before deploy --production. If no handoff could be minted, stagingPreview.reason names why and "
|
|
406
|
-
+ "stagingPreview.recovery the next step; 'staging review' mints another."
|
|
555
|
+
+ "stagingPreview.recovery the next step; 'staging review' mints another. On a Standard site the result also "
|
|
556
|
+
+ "carries the automatic redirect check: routeCheck and redirects (see 'help redirects').",
|
|
407
557
|
},
|
|
408
558
|
{
|
|
409
559
|
name: VERB_PREVIEW_PAGE,
|
|
@@ -591,7 +741,7 @@ ${COMMON_OPTIONS}
|
|
|
591
741
|
function verbHelp(verb) {
|
|
592
742
|
const targetUsage = verb.target ? " (--staging | --production)" : "";
|
|
593
743
|
const positionalUsage = verb.positionals === "fixturePath"
|
|
594
|
-
? " <
|
|
744
|
+
? " [<directory>]"
|
|
595
745
|
: verb.positionals === "pageSelector"
|
|
596
746
|
? " <page-path-or-id>"
|
|
597
747
|
: verb.positionals === "previewIds"
|
|
@@ -602,6 +752,14 @@ function verbHelp(verb) {
|
|
|
602
752
|
? " <site-name-or-id>"
|
|
603
753
|
: verb.positionals === "formKeys"
|
|
604
754
|
? " [form-key...]"
|
|
755
|
+
: verb.positionals === "placeQuery"
|
|
756
|
+
? " <query...>"
|
|
757
|
+
: verb.positionals === "placeSelection"
|
|
758
|
+
? " <google-place-id> <session-token>"
|
|
759
|
+
: verb.positionals === "metaPagePath"
|
|
760
|
+
? " <page-path> [--title <text>] [--description <text>] [--author <handle-or-email>]"
|
|
761
|
+
: verb.positionals === "authorHandle"
|
|
762
|
+
? " <handle> --name <display-name> [--email <address>]"
|
|
605
763
|
: verb.positionals
|
|
606
764
|
? ` [${verb.positionals === "paths" ? "path" : "page-path"}...]`
|
|
607
765
|
: "";
|
|
@@ -615,12 +773,24 @@ function verbHelp(verb) {
|
|
|
615
773
|
--production Promote the completed staging deployment to production.
|
|
616
774
|
--allow-failed-preview Explicitly override the matching candidate's failed preview.`
|
|
617
775
|
: "";
|
|
776
|
+
const metaOptions = verb.metaOptions
|
|
777
|
+
? `\n --title <text> The page's title.
|
|
778
|
+
--description <text> The page's description for search results and link previews; '' clears it.
|
|
779
|
+
--author <ref> Credit a page that has no author: a site author's handle or a member's email address.`
|
|
780
|
+
: "";
|
|
781
|
+
const authorOptions = verb.authorOptions
|
|
782
|
+
? `\n --name <text> The name readers see on the byline (required).
|
|
783
|
+
--email <address> A private address, kept to link the person to an account later (optional).`
|
|
784
|
+
: "";
|
|
618
785
|
const jsonOption = verb.json
|
|
619
786
|
? "\n --json Emit the stable JSON contract (operational output is always JSON)."
|
|
620
787
|
: "";
|
|
621
788
|
const dryRunOption = verb.dryRun
|
|
622
789
|
? "\n --dry-run Read the site and report what a push would change, without writing."
|
|
623
790
|
: "";
|
|
791
|
+
const planOption = verb.planHash
|
|
792
|
+
? "\n --plan <hash> The planHash 'plan' reported; required."
|
|
793
|
+
: "";
|
|
624
794
|
const nameOption = verb.keyName
|
|
625
795
|
? `\n --name <text> Name recorded on the issued key (default "${DEFAULT_LOGIN_KEY_NAME}",\n`
|
|
626
796
|
+ ` 1-${LOGIN_KEY_NAME_MAXIMUM} characters). The approval screen shows it.`
|
|
@@ -644,11 +814,15 @@ function verbHelp(verb) {
|
|
|
644
814
|
+ "Taproot accepts only the latest; with nothing recorded it runs."
|
|
645
815
|
: "";
|
|
646
816
|
const boundary = verb.name === VERB_VALIDATE
|
|
647
|
-
? "Validation uses no credential,
|
|
648
|
-
+
|
|
817
|
+
? "Validation uses no credential, network or write; it reads the configuration only to find the workspace when "
|
|
818
|
+
+ "no directory is given. With --init it reads the pulled workspace or selected configuration and writes a new "
|
|
819
|
+
+ `fixture directory, still without credentials or network.${upgradeGate}`
|
|
649
820
|
: selfContained
|
|
650
821
|
? "This offline verb uses no credential and reads no configuration, and performs no network request and no "
|
|
651
822
|
+ `write.${upgradeGate}`
|
|
823
|
+
: verb.writesWorkspace
|
|
824
|
+
? "This verb edits only the local workspace: it reads the configuration, uses no credential, and makes no "
|
|
825
|
+
+ "network request."
|
|
652
826
|
: verb.offline
|
|
653
827
|
? "This verb answers entirely from local state — the stored sign-in and the configuration — and makes no "
|
|
654
828
|
+ `network request and no write.${upgradeGate}`
|
|
@@ -661,7 +835,8 @@ function verbHelp(verb) {
|
|
|
661
835
|
const options = verb.name === VERB_VALIDATE
|
|
662
836
|
? `Options:
|
|
663
837
|
--init Export a pulled workspace into the new fixture directory.
|
|
664
|
-
--config <path> Before the verb;
|
|
838
|
+
--config <path> Before the verb; the configuration naming the workspace (no
|
|
839
|
+
directory) or the --init source.
|
|
665
840
|
--quiet Suppress human progress. The JSON result is unchanged.
|
|
666
841
|
--help Show this help.
|
|
667
842
|
--version Show the package version.`
|
|
@@ -688,7 +863,7 @@ function verbHelp(verb) {
|
|
|
688
863
|
${verb.summary}
|
|
689
864
|
${boundary}
|
|
690
865
|
|
|
691
|
-
${options}${targetOption}${deliveryTargetOption}${jsonOption}${dryRunOption}${nameOption}${note}
|
|
866
|
+
${options}${targetOption}${deliveryTargetOption}${metaOptions}${authorOptions}${jsonOption}${dryRunOption}${planOption}${nameOption}${note}
|
|
692
867
|
`;
|
|
693
868
|
}
|
|
694
869
|
|
|
@@ -761,6 +936,9 @@ function parseReferenceArguments(arguments_) {
|
|
|
761
936
|
) {
|
|
762
937
|
throw usageError("help.usage", `The '${topic}' topic does not accept a name.`);
|
|
763
938
|
}
|
|
939
|
+
if (topic === "fonts" && extra.length > 0) {
|
|
940
|
+
throw usageError("help.usage", "The 'fonts' topic accepts at most one category.");
|
|
941
|
+
}
|
|
764
942
|
if ((topic === "page" || topic === "component" || topic === "design") && (subject === undefined || extra.length > 0)) {
|
|
765
943
|
throw usageError("help.usage", `The '${topic}' topic requires exactly one name.`);
|
|
766
944
|
}
|
|
@@ -833,6 +1011,17 @@ function referenceResult(parsed) {
|
|
|
833
1011
|
referenceKind: parsed.topic,
|
|
834
1012
|
reference: getWorkflowReference(parsed.topic),
|
|
835
1013
|
};
|
|
1014
|
+
case "fonts": {
|
|
1015
|
+
const fonts = getFontCatalogReference(parsed.subject);
|
|
1016
|
+
if (!fonts) {
|
|
1017
|
+
throw usageError(
|
|
1018
|
+
"help.font_category_unknown",
|
|
1019
|
+
`Unknown font category. Expected one of: ${FONT_CATEGORIES.join(", ")}.`,
|
|
1020
|
+
{ alternatives: FONT_CATEGORIES },
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
return { ...result, topic: "fonts", fonts };
|
|
1024
|
+
}
|
|
836
1025
|
case "theme":
|
|
837
1026
|
return { ...result, topic: "presentation", referenceKind: "theme", reference: getThemeReference() };
|
|
838
1027
|
case "appearance":
|
|
@@ -896,6 +1085,55 @@ function parseWaitOption(arguments_, index) {
|
|
|
896
1085
|
return seconds;
|
|
897
1086
|
}
|
|
898
1087
|
|
|
1088
|
+
function parsePlanOption(arguments_, index) {
|
|
1089
|
+
const candidate = arguments_[index + 1];
|
|
1090
|
+
if (typeof candidate !== "string" || !PLAN_HASH.test(candidate)) {
|
|
1091
|
+
throw usageError("apply.plan_required", "--plan requires the planHash 'plan' reported (sha256:<64 hex>).", {
|
|
1092
|
+
field: "planHash",
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
return candidate;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* The text after --title or --description. Either may be empty, which clears a
|
|
1100
|
+
* generated page's custom value; a page with its own title refuses an empty one.
|
|
1101
|
+
*/
|
|
1102
|
+
function parseMetaOption(arguments_, index, option) {
|
|
1103
|
+
const candidate = arguments_[index + 1];
|
|
1104
|
+
if (
|
|
1105
|
+
typeof candidate !== "string"
|
|
1106
|
+
|| Buffer.byteLength(candidate, "utf8") > 8 * 1024
|
|
1107
|
+
|| hasAsciiControl(candidate)
|
|
1108
|
+
) {
|
|
1109
|
+
throw usageError(
|
|
1110
|
+
"cli.meta_option",
|
|
1111
|
+
option === "--title"
|
|
1112
|
+
? "--title requires one printable title (an empty one clears a generated page's custom title)."
|
|
1113
|
+
: "--description requires one printable description (an empty one clears it).",
|
|
1114
|
+
{ field: option.slice(2) },
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
return candidate;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** The text after --author, --name (authors add) or --email: one printable value, bounded. */
|
|
1121
|
+
function parseAuthorOption(arguments_, index, option, { allowEmpty = false } = {}) {
|
|
1122
|
+
const candidate = arguments_[index + 1];
|
|
1123
|
+
if (
|
|
1124
|
+
typeof candidate !== "string"
|
|
1125
|
+
|| (candidate.length === 0 && !allowEmpty)
|
|
1126
|
+
|| Buffer.byteLength(candidate, "utf8") > 2 * 1024
|
|
1127
|
+
|| hasAsciiControl(candidate)
|
|
1128
|
+
|| candidate.startsWith("--")
|
|
1129
|
+
) {
|
|
1130
|
+
throw usageError("cli.author_option", `${option} requires exactly one printable value.`, {
|
|
1131
|
+
field: option.slice(2),
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
return candidate;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
899
1137
|
function parseNameOption(arguments_, index) {
|
|
900
1138
|
const candidate = arguments_[index + 1];
|
|
901
1139
|
if (
|
|
@@ -948,10 +1186,11 @@ function parseArguments(arguments_) {
|
|
|
948
1186
|
// origin the sign-in belongs to — so they accept it. whoami is offline and
|
|
949
1187
|
// reads both the store and the configuration, so it accepts it too: the test
|
|
950
1188
|
// is "reads nothing local", not "makes no request".
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1189
|
+
// validate reads the configuration in two cases: --init, and no directory,
|
|
1190
|
+
// which checks the workspace the configuration names.
|
|
1191
|
+
const validateReadsConfig = verb.name === VERB_VALIDATE
|
|
1192
|
+
&& (rest.includes("--init") || rest.every((argument) => argument.startsWith("-")));
|
|
1193
|
+
if (verb.offline && !verb.readsLocalState && configPath !== undefined && !validateReadsConfig) {
|
|
955
1194
|
throw usageError("cli.config_option", "--config applies only to verbs that read the site configuration.", {
|
|
956
1195
|
field: "configPath",
|
|
957
1196
|
});
|
|
@@ -969,6 +1208,12 @@ function parseArguments(arguments_) {
|
|
|
969
1208
|
let dryRun = false;
|
|
970
1209
|
let json = false;
|
|
971
1210
|
let keyName;
|
|
1211
|
+
let planHash;
|
|
1212
|
+
let metaTitle;
|
|
1213
|
+
let metaDescription;
|
|
1214
|
+
let metaAuthor;
|
|
1215
|
+
let authorName;
|
|
1216
|
+
let authorEmail;
|
|
972
1217
|
const positionals = [];
|
|
973
1218
|
for (let index = 0; index < rest.length; index += 1) {
|
|
974
1219
|
const argument = rest[index];
|
|
@@ -1035,6 +1280,45 @@ function parseArguments(arguments_) {
|
|
|
1035
1280
|
json = true;
|
|
1036
1281
|
continue;
|
|
1037
1282
|
}
|
|
1283
|
+
if (verb.planHash && argument === "--plan") {
|
|
1284
|
+
if (planHash !== undefined) throw usageError("cli.duplicate_option", "--plan may be supplied only once.");
|
|
1285
|
+
planHash = parsePlanOption(rest, index);
|
|
1286
|
+
index += 1;
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (verb.metaOptions && (argument === "--title" || argument === "--description")) {
|
|
1290
|
+
const value = parseMetaOption(rest, index, argument);
|
|
1291
|
+
if (argument === "--title") {
|
|
1292
|
+
if (metaTitle !== undefined) throw usageError("cli.duplicate_option", "--title may be supplied only once.");
|
|
1293
|
+
metaTitle = value;
|
|
1294
|
+
} else {
|
|
1295
|
+
if (metaDescription !== undefined) {
|
|
1296
|
+
throw usageError("cli.duplicate_option", "--description may be supplied only once.");
|
|
1297
|
+
}
|
|
1298
|
+
metaDescription = value;
|
|
1299
|
+
}
|
|
1300
|
+
index += 1;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
if (verb.metaOptions && argument === "--author") {
|
|
1304
|
+
if (metaAuthor !== undefined) throw usageError("cli.duplicate_option", "--author may be supplied only once.");
|
|
1305
|
+
// An empty value clears a pending author that was never sent.
|
|
1306
|
+
metaAuthor = parseAuthorOption(rest, index, argument, { allowEmpty: true });
|
|
1307
|
+
index += 1;
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (verb.authorOptions && (argument === "--name" || argument === "--email")) {
|
|
1311
|
+
const value = parseAuthorOption(rest, index, argument);
|
|
1312
|
+
if (argument === "--name") {
|
|
1313
|
+
if (authorName !== undefined) throw usageError("cli.duplicate_option", "--name may be supplied only once.");
|
|
1314
|
+
authorName = value;
|
|
1315
|
+
} else {
|
|
1316
|
+
if (authorEmail !== undefined) throw usageError("cli.duplicate_option", "--email may be supplied only once.");
|
|
1317
|
+
authorEmail = value;
|
|
1318
|
+
}
|
|
1319
|
+
index += 1;
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1038
1322
|
if (verb.keyName && argument === "--name") {
|
|
1039
1323
|
if (keyName !== undefined) throw usageError("cli.duplicate_option", "--name may be supplied only once.");
|
|
1040
1324
|
keyName = parseNameOption(rest, index);
|
|
@@ -1066,13 +1350,10 @@ function parseArguments(arguments_) {
|
|
|
1066
1350
|
`${verb.tokens.join(" ")} requires exactly one of --staging or --production.`,
|
|
1067
1351
|
);
|
|
1068
1352
|
}
|
|
1069
|
-
if (
|
|
1070
|
-
verb.positionals === "fixturePath"
|
|
1071
|
-
&& positionals.length !== 1
|
|
1072
|
-
) {
|
|
1353
|
+
if (verb.positionals === "fixturePath" && (positionals.length > 1 || (init && positionals.length !== 1))) {
|
|
1073
1354
|
throw usageError(
|
|
1074
1355
|
"validate.fixture_path_invalid",
|
|
1075
|
-
"validate requires exactly one fixture directory.",
|
|
1356
|
+
init ? "validate --init requires exactly one new fixture directory." : "validate accepts at most one directory.",
|
|
1076
1357
|
{ field: "fixturePath" },
|
|
1077
1358
|
);
|
|
1078
1359
|
}
|
|
@@ -1120,6 +1401,33 @@ function parseArguments(arguments_) {
|
|
|
1120
1401
|
{ field: "environmentSelector" },
|
|
1121
1402
|
);
|
|
1122
1403
|
}
|
|
1404
|
+
if (verb.positionals === "metaPagePath" && positionals.length !== 1) {
|
|
1405
|
+
throw usageError("pages.meta_path_invalid", "pages meta set names exactly one page path ('/' for the homepage).", {
|
|
1406
|
+
field: "pagePath",
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
if (verb.metaOptions && metaTitle === undefined && metaDescription === undefined && metaAuthor === undefined) {
|
|
1410
|
+
throw usageError(
|
|
1411
|
+
"pages.meta_nothing_to_set",
|
|
1412
|
+
"pages meta set needs --title, --description, --author, or any of them.",
|
|
1413
|
+
{ field: "title" },
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
if (verb.positionals === "authorHandle" && positionals.length !== 1) {
|
|
1417
|
+
throw usageError("authors.handle_missing", "authors add needs exactly one handle, such as jane-doe.", {
|
|
1418
|
+
field: "handle",
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
if (verb.authorOptions && authorName === undefined) {
|
|
1422
|
+
throw usageError("authors.name_missing", "authors add needs --name, the name readers see on the byline.", {
|
|
1423
|
+
field: "name",
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
if (verb.planHash && planHash === undefined) {
|
|
1427
|
+
throw usageError("apply.plan_required", "apply requires --plan with the planHash that 'plan' reported.", {
|
|
1428
|
+
field: "planHash",
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1123
1431
|
if (quiet && verb.name === VERB_LOGIN) {
|
|
1124
1432
|
// The approval URL and user code reach the operator only as progress
|
|
1125
1433
|
// lines, and the JSON result is serialized only after polling ends — so a
|
|
@@ -1161,12 +1469,19 @@ function parseArguments(arguments_) {
|
|
|
1161
1469
|
init,
|
|
1162
1470
|
dryRun,
|
|
1163
1471
|
keyName,
|
|
1472
|
+
...(planHash === undefined ? {} : { planHash }),
|
|
1473
|
+
...(metaTitle === undefined ? {} : { metaTitle }),
|
|
1474
|
+
...(metaDescription === undefined ? {} : { metaDescription }),
|
|
1475
|
+
...(metaAuthor === undefined ? {} : { metaAuthor }),
|
|
1476
|
+
...(authorName === undefined ? {} : { authorName }),
|
|
1477
|
+
...(authorEmail === undefined ? {} : { authorEmail }),
|
|
1164
1478
|
positionals: verb.positionals
|
|
1165
1479
|
? {
|
|
1166
1480
|
key: verb.positionals,
|
|
1167
1481
|
values: positionals,
|
|
1168
|
-
scalar: verb.positionals === "pageSelector" || verb.positionals === "fixturePath"
|
|
1169
|
-
|| verb.positionals === "siteSelector" || verb.positionals === "environmentSelector"
|
|
1482
|
+
scalar: verb.positionals === "pageSelector" || verb.positionals === "fixturePath" || verb.positionals === "metaPagePath"
|
|
1483
|
+
|| verb.positionals === "siteSelector" || verb.positionals === "environmentSelector"
|
|
1484
|
+
|| verb.positionals === "authorHandle",
|
|
1170
1485
|
}
|
|
1171
1486
|
: undefined,
|
|
1172
1487
|
};
|
|
@@ -1244,6 +1559,16 @@ export async function runCli({
|
|
|
1244
1559
|
quiet: parsed.quiet,
|
|
1245
1560
|
dryRun: parsed.dryRun,
|
|
1246
1561
|
keyName: parsed.keyName,
|
|
1562
|
+
...(parsed.planHash === undefined ? {} : { planHash: parsed.planHash }),
|
|
1563
|
+
// Only when given, like the other optional fields, so each verb keeps its default (TR00925).
|
|
1564
|
+
...(parsed.deliveryUrl === undefined ? {} : { deliveryUrl: parsed.deliveryUrl }),
|
|
1565
|
+
...(parsed.propagationWaitSeconds === undefined ? {} : { propagationWaitSeconds: parsed.propagationWaitSeconds }),
|
|
1566
|
+
...(parsed.browser === undefined ? {} : { browser: parsed.browser }),
|
|
1567
|
+
...(parsed.metaTitle === undefined ? {} : { metaTitle: parsed.metaTitle }),
|
|
1568
|
+
...(parsed.metaDescription === undefined ? {} : { metaDescription: parsed.metaDescription }),
|
|
1569
|
+
...(parsed.metaAuthor === undefined ? {} : { metaAuthor: parsed.metaAuthor }),
|
|
1570
|
+
...(parsed.authorName === undefined ? {} : { authorName: parsed.authorName }),
|
|
1571
|
+
...(parsed.authorEmail === undefined ? {} : { authorEmail: parsed.authorEmail }),
|
|
1247
1572
|
capabilities: parsed.capabilities,
|
|
1248
1573
|
surface: parsed.surface,
|
|
1249
1574
|
surfaceCapabilities: parsed.surfaceCapabilities,
|
package/src/constants.js
CHANGED
|
@@ -83,6 +83,9 @@ export const VERB_VALIDATE = "validate";
|
|
|
83
83
|
export const VERB_LOGIN = "login";
|
|
84
84
|
export const VERB_LOGOUT = "logout";
|
|
85
85
|
export const VERB_PAGES_PUSH = "pages push";
|
|
86
|
+
export const VERB_PAGES_META_SET = "pages meta set";
|
|
87
|
+
export const VERB_AUTHORS_LIST = "authors list";
|
|
88
|
+
export const VERB_AUTHORS_ADD = "authors add";
|
|
86
89
|
export const VERB_NAV_PUSH = "nav push";
|
|
87
90
|
export const VERB_REDIRECTS_CHECK = "redirects check";
|
|
88
91
|
export const VERB_REDIRECTS_PULL = "redirects pull";
|
|
@@ -103,6 +106,12 @@ export const VERB_STAGING_REVIEW = "staging review";
|
|
|
103
106
|
export const VERB_SITES = "sites";
|
|
104
107
|
export const VERB_USE = "use";
|
|
105
108
|
export const VERB_WHOAMI = "whoami";
|
|
109
|
+
export const VERB_PLAN = "plan";
|
|
110
|
+
export const VERB_APPLY = "apply";
|
|
111
|
+
export const VERB_PLACES_SEARCH = "places search";
|
|
112
|
+
export const VERB_PLACES_SELECT = "places select";
|
|
113
|
+
/** The identity `plan` reports and `apply --plan` requires. */
|
|
114
|
+
export const PLAN_HASH = /^sha256:[0-9a-f]{64}$/u;
|
|
106
115
|
export const VERB_ENV = "env";
|
|
107
116
|
|
|
108
117
|
// Which authoring verbs a site accepts (TR00790), as `use` records it in the
|