@taprootio/site-authoring 0.20.0 → 0.21.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 +114 -28
- package/examples/riverbend-wellness/nav.json +3 -6
- package/examples/riverbend-wellness/pages/visit.md +1 -1
- package/package.json +2 -2
- package/src/api.js +118 -31
- package/src/changelog.js +48 -0
- package/src/changelog.json +71 -0
- package/src/cli-release.js +9 -1
- package/src/cli.js +122 -13
- package/src/constants.js +19 -0
- package/src/errors.js +3 -0
- package/src/footer-contract.js +5 -3
- package/src/index.js +1 -0
- package/src/output.js +9 -0
- package/src/presentation-reference.js +41 -6
- package/src/problems.js +6 -1
- package/src/redirects-contract.js +50 -140
- package/src/reference-help.js +77 -33
- package/src/reserved-paths.js +4 -10
- package/src/setting-reference.js +438 -0
- package/src/site-check-findings.js +99 -0
- package/src/site-check-rules.json +22 -0
- package/src/site-checks.js +337 -0
- package/src/staging-check.js +48 -9
- package/src/staging-served.js +320 -0
- package/src/transport.js +13 -0
- package/src/typed-page-reference.js +128 -4
- package/src/verbs/changelog.js +62 -0
- package/src/verbs/deploy.js +38 -10
- package/src/verbs/index.js +3 -0
- package/src/verbs/nav-push.js +49 -1
- package/src/verbs/pull.js +5 -1
- package/src/verbs/redirects-check.js +4 -1
- package/src/verbs/staging-review.js +1 -1
- package/src/verbs/status.js +35 -28
- package/src/verbs/validate.js +21 -0
package/src/cli.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeStagingReviewPath } from "./api.js";
|
|
2
|
+
import { parseCliVersion } from "./cli-release.js";
|
|
1
3
|
import { CAPABILITY_CONTENT, CAPABILITY_DEPLOYMENTS, CAPABILITY_DESIGN } from "./capabilities.js";
|
|
2
4
|
import { capabilitiesForSurface } from "./surface.js";
|
|
3
5
|
import {
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
VERB_DELIVERY_CHECK,
|
|
23
25
|
VERB_DEPLOY,
|
|
24
26
|
VERB_STAGING_REVIEW,
|
|
27
|
+
VERB_CHANGELOG,
|
|
25
28
|
VERB_ENV,
|
|
26
29
|
VERB_FOOTER_PUSH,
|
|
27
30
|
VERB_FORMS_PULL,
|
|
@@ -69,6 +72,7 @@ import {
|
|
|
69
72
|
getDesignBlueprint,
|
|
70
73
|
getFooterReference,
|
|
71
74
|
getPageTypeReference,
|
|
75
|
+
getSettingsReference,
|
|
72
76
|
getThemeReference,
|
|
73
77
|
getWorkflowReference,
|
|
74
78
|
listComponentTypeReferences,
|
|
@@ -118,7 +122,12 @@ const VERBS = Object.freeze([
|
|
|
118
122
|
+ "rather than one per run. A pulled workspace is checked offline against the pages (generated ones included, "
|
|
119
123
|
+ "so navigation to a section or tag page resolves), images, and baselines its manifests recorded at the pull. "
|
|
120
124
|
+ "A link whose address is only http(s):// plus its own text (left by older editor autolinking) is reported in "
|
|
121
|
-
+ "linkWarnings without refusing; pages push reports the same for the pages it sends.
|
|
125
|
+
+ "linkWarnings without refusing; pages push reports the same for the pages it sends. "
|
|
126
|
+
+ "The site checks that can run offline are reported in findings (a rule id, severity, file, and the value "
|
|
127
|
+
+ "found, such as link.page_missing for a link to a page the workspace does not have) and never refuse a "
|
|
128
|
+
+ "warning; siteChecks.checkedBySite names the rules that need the site's own records (images, forms), which "
|
|
129
|
+
+ "'status' reports under siteHealth. A redirect source the site would refuse names its rule in "
|
|
130
|
+
+ "error.problems[].rule. This proves local structure and semantics only. It does not prove "
|
|
122
131
|
+ "authorization, live site ownership, concurrency, persisted round trips, or rendering: 'plan' checks a "
|
|
123
132
|
+ "workspace against the live site, and 'pages push --dry-run' does that for pages alone. "
|
|
124
133
|
+ `See '${CLI_BINARY_NAME} help fixture' for the fixture manifest contract and for the path of the complete `
|
|
@@ -200,6 +209,21 @@ const VERBS = Object.freeze([
|
|
|
200
209
|
+ "its value. The sign-in secret is never printed; the credential is named by id and display prefix, which are "
|
|
201
210
|
+ "what an owner needs to revoke it.",
|
|
202
211
|
},
|
|
212
|
+
{
|
|
213
|
+
name: VERB_CHANGELOG,
|
|
214
|
+
tokens: ["changelog"],
|
|
215
|
+
summary: "List what changed in each CLI release, offline.",
|
|
216
|
+
credentialFree: true,
|
|
217
|
+
configFree: true,
|
|
218
|
+
offline: true,
|
|
219
|
+
sinceOption: true,
|
|
220
|
+
note: "Only the latest CLI is accepted, so an upgrade can be forced at any time. After one, run "
|
|
221
|
+
+ "'changelog --since <the version you upgraded from>' to see every release's note and the behavior it changed, "
|
|
222
|
+
+ "and which help topics (help <topic>) to read again. With no --since it prints the installed release's entry. "
|
|
223
|
+
+ "It reads the package's own record, needs no sign-in or site, and still runs when this CLI is outdated. "
|
|
224
|
+
+ "The result lists entries newest first, each { version, note, behavior, help }; complete is false when "
|
|
225
|
+
+ "--since starts before the oldest release the record covers. 'status' reports the installed release's note.",
|
|
226
|
+
},
|
|
203
227
|
{
|
|
204
228
|
name: VERB_ENV,
|
|
205
229
|
tokens: ["env"],
|
|
@@ -440,9 +464,12 @@ const VERBS = Object.freeze([
|
|
|
440
464
|
capabilities: [CAPABILITY_CONTENT, CAPABILITY_DEPLOYMENTS],
|
|
441
465
|
tokens: ["redirects", "check"],
|
|
442
466
|
summary: "Check the current redirect map through the authenticated staging edge.",
|
|
467
|
+
handoffOptions: true,
|
|
443
468
|
note: "Reports each entry's path, HTTP status and Location in redirects.items, and redirects.verified, without "
|
|
444
469
|
+ "following redirects. A redirect is live only with the release that carries it, so this reads the staged release. "
|
|
445
|
-
+ "The
|
|
470
|
+
+ "The check authenticates with its own handoff and never reports it. The result carries no review URL unless "
|
|
471
|
+
+ "you pass --handoff, which adds a fresh single-use one in stagingPreview.url (keep it private); "
|
|
472
|
+
+ "'staging review' mints one at any time.",
|
|
446
473
|
},
|
|
447
474
|
{
|
|
448
475
|
name: VERB_REDIRECTS_PULL,
|
|
@@ -589,11 +616,19 @@ const VERBS = Object.freeze([
|
|
|
589
616
|
tokens: ["deploy"],
|
|
590
617
|
summary: "Deploy to staging, or promote staging to production.",
|
|
591
618
|
target: true,
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
+ "
|
|
619
|
+
handoffOptions: true,
|
|
620
|
+
note: "deploy --staging does not report a review URL, because the URL is a live single-use credential and this "
|
|
621
|
+
+ "JSON is often saved to logs. Run 'staging review' to mint one, or pass --handoff to get a fresh "
|
|
622
|
+
+ "single-use one in stagingPreview.url (on a managed Docs site too, where it stages settings only): open it "
|
|
623
|
+
+ "once, then switch the site's theme toggle to review both schemes before deploy --production. Without "
|
|
624
|
+
+ "--handoff, stagingPreview.reason is staging.handoff_not_requested and stagingPreview.recovery names the "
|
|
625
|
+
+ "command. If a requested handoff could not be minted, stagingPreview.reason names why and "
|
|
595
626
|
+ "stagingPreview.recovery the next step; 'staging review' mints another. On a Standard site the result also "
|
|
596
|
-
+ "carries the automatic redirect check: routeCheck and redirects (see 'help redirects')."
|
|
627
|
+
+ "carries the automatic redirect check: routeCheck and redirects (see 'help redirects'). The edge can keep "
|
|
628
|
+
+ `serving the previous release for up to ${LIMITS.stagingPropagationCeilingSeconds} seconds after a deployment completes, so a Standard site's `
|
|
629
|
+
+ "deploy --staging waits until staging serves the new release and reports stagingServed: state served, "
|
|
630
|
+
+ "propagating (still the previous release at the bound) or unverified (the change cannot be attributed to this deployment, or the last read did not name the previous release), "
|
|
631
|
+
+ "with the time to wait or the seconds waited.",
|
|
597
632
|
},
|
|
598
633
|
{
|
|
599
634
|
name: VERB_PREVIEW_PAGE,
|
|
@@ -653,20 +688,23 @@ const VERBS = Object.freeze([
|
|
|
653
688
|
capabilities: [CAPABILITY_DEPLOYMENTS],
|
|
654
689
|
tokens: ["staging", "review"],
|
|
655
690
|
summary: "Mint a fresh single-use review handoff for the site's staging host.",
|
|
691
|
+
reviewPath: true,
|
|
656
692
|
note: "The handoff URL is reported only in the final JSON as stagingPreview.url, is single-use, and expires two "
|
|
657
|
-
+ "minutes after it is minted; keep it private.
|
|
658
|
-
+ "
|
|
693
|
+
+ "minutes after it is minted; keep it private. --path <page> opens the review on that page instead of the "
|
|
694
|
+
+ "home page: a page path as 'pull' records it ('about', '/posts/one'), optionally with a query. Open it once, then use the site's theme toggle to review both "
|
|
695
|
+
+ "schemes. deploy --staging and redirects check report no review URL unless you pass --handoff, so this is "
|
|
696
|
+
+ "the usual way to get one, on a managed Docs site too.",
|
|
659
697
|
},
|
|
660
698
|
{
|
|
661
699
|
name: VERB_STATUS,
|
|
662
700
|
surface: SURFACE_DOCS_PRESENTATION,
|
|
663
701
|
// Readiness and the deployment log are Deployments; the image list is
|
|
664
|
-
// Content, as is the
|
|
702
|
+
// Content, as is the site-health read.
|
|
665
703
|
capabilities: [CAPABILITY_CONTENT, CAPABILITY_DEPLOYMENTS],
|
|
666
704
|
tokens: ["status"],
|
|
667
705
|
summary:
|
|
668
|
-
"Report the platform authoring switch, CLI release, deployments, readiness, image processing, and
|
|
669
|
-
note: "
|
|
706
|
+
"Report the platform authoring switch, CLI release, deployments, readiness, image processing, and site health.",
|
|
707
|
+
note: "Site health lists the site checks' findings (a rule id, severity, page, and the value found) for the latest editable body of each page."
|
|
670
708
|
+ " Content permission is required; a refused or failed read fails status rather than reporting a clean site.",
|
|
671
709
|
},
|
|
672
710
|
]);
|
|
@@ -733,7 +771,8 @@ credentials, and it expires 24 hours after it is issued.
|
|
|
733
771
|
|
|
734
772
|
Verbs write one JSON result to stdout and human progress to stderr. validate,
|
|
735
773
|
help, whoami, and env are offline and read-only. login, logout, sites, use,
|
|
736
|
-
whoami, and env need no configuration and no site.
|
|
774
|
+
whoami, and env need no configuration and no site. So does changelog, which
|
|
775
|
+
also still runs when this CLI is outdated. The offline help family is
|
|
737
776
|
human-readable by default; add --json for stable reference data. Exit codes: 0
|
|
738
777
|
success, 1 failure, 2 usage fault.
|
|
739
778
|
|
|
@@ -817,6 +856,18 @@ function verbHelp(verb) {
|
|
|
817
856
|
--production Promote the completed staging deployment to production.
|
|
818
857
|
--allow-failed-preview Explicitly override the matching candidate's failed preview.`
|
|
819
858
|
: "";
|
|
859
|
+
const handoffOption = verb.handoffOptions
|
|
860
|
+
? `\n --handoff Also mint a fresh single-use review URL and report it in stagingPreview.url.
|
|
861
|
+
--no-handoff Report no review URL (the default); accepted so scripts can say so explicitly.`
|
|
862
|
+
: "";
|
|
863
|
+
const sinceOption = verb.sinceOption
|
|
864
|
+
? "\n --since <version> List the releases after this one, up to the installed release."
|
|
865
|
+
: "";
|
|
866
|
+
const reviewPathOption = verb.reviewPath
|
|
867
|
+
? "\n --path <page> Open the review on this page instead of the home page, such as 'about' or '/posts/one'."
|
|
868
|
+
: verb.handoffOptions
|
|
869
|
+
? "\n --review-path <page> With --handoff, open the review URL on this page instead of the home page."
|
|
870
|
+
: "";
|
|
820
871
|
const metaOptions = verb.metaOptions
|
|
821
872
|
? `\n --title <text> The page's title.
|
|
822
873
|
--description <text> The page's description for search results and link previews; '' clears it.
|
|
@@ -907,7 +958,7 @@ function verbHelp(verb) {
|
|
|
907
958
|
${verb.summary}
|
|
908
959
|
${boundary}
|
|
909
960
|
|
|
910
|
-
${options}${targetOption}${deliveryTargetOption}${metaOptions}${authorOptions}${jsonOption}${dryRunOption}${planOption}${nameOption}${note}
|
|
961
|
+
${options}${targetOption}${handoffOption}${reviewPathOption}${sinceOption}${deliveryTargetOption}${metaOptions}${authorOptions}${jsonOption}${dryRunOption}${planOption}${nameOption}${note}
|
|
911
962
|
`;
|
|
912
963
|
}
|
|
913
964
|
|
|
@@ -968,6 +1019,7 @@ function parseReferenceArguments(arguments_) {
|
|
|
968
1019
|
|| topic === "preview"
|
|
969
1020
|
|| topic === "theme"
|
|
970
1021
|
|| topic === "appearance"
|
|
1022
|
+
|| topic === "settings"
|
|
971
1023
|
|| topic === "footer"
|
|
972
1024
|
|| topic === "fixture"
|
|
973
1025
|
|| topic === "redirects"
|
|
@@ -1070,6 +1122,8 @@ function referenceResult(parsed) {
|
|
|
1070
1122
|
return { ...result, topic: "presentation", referenceKind: "theme", reference: getThemeReference() };
|
|
1071
1123
|
case "appearance":
|
|
1072
1124
|
return { ...result, topic: "presentation", referenceKind: "appearance", reference: getAppearanceReference() };
|
|
1125
|
+
case "settings":
|
|
1126
|
+
return { ...result, topic: "presentation", referenceKind: "settings", reference: getSettingsReference() };
|
|
1073
1127
|
case "footer":
|
|
1074
1128
|
return { ...result, topic: "presentation", referenceKind: "footer", reference: getFooterReference() };
|
|
1075
1129
|
default:
|
|
@@ -1249,6 +1303,9 @@ function parseArguments(arguments_) {
|
|
|
1249
1303
|
let propagationWaitSeconds;
|
|
1250
1304
|
let browser;
|
|
1251
1305
|
let allowFailedPreview = false;
|
|
1306
|
+
let handoff;
|
|
1307
|
+
let reviewPath;
|
|
1308
|
+
let since;
|
|
1252
1309
|
let dryRun = false;
|
|
1253
1310
|
let json = false;
|
|
1254
1311
|
let keyName;
|
|
@@ -1314,6 +1371,42 @@ function parseArguments(arguments_) {
|
|
|
1314
1371
|
allowFailedPreview = true;
|
|
1315
1372
|
continue;
|
|
1316
1373
|
}
|
|
1374
|
+
if (verb.handoffOptions && (argument === "--handoff" || argument === "--no-handoff")) {
|
|
1375
|
+
if (handoff !== undefined) {
|
|
1376
|
+
throw usageError(
|
|
1377
|
+
handoff === (argument === "--handoff") ? "cli.duplicate_option" : "cli.handoff_conflict",
|
|
1378
|
+
handoff === (argument === "--handoff")
|
|
1379
|
+
? `${argument} may be supplied only once.`
|
|
1380
|
+
: "--handoff and --no-handoff cannot be used together.",
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
handoff = argument === "--handoff";
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
if ((verb.reviewPath && argument === "--path") || (verb.handoffOptions && argument === "--review-path")) {
|
|
1387
|
+
if (reviewPath !== undefined) {
|
|
1388
|
+
throw usageError("cli.duplicate_option", `${argument} may be supplied only once.`);
|
|
1389
|
+
}
|
|
1390
|
+
// An option-like value would otherwise become the page '/--json' and mint a handoff to a 404.
|
|
1391
|
+
const candidate = rest[index + 1];
|
|
1392
|
+
reviewPath = normalizeStagingReviewPath(typeof candidate === "string" && candidate.startsWith("--") ? "" : candidate);
|
|
1393
|
+
index += 1;
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (verb.sinceOption && argument === "--since") {
|
|
1397
|
+
if (since !== undefined) throw usageError("cli.duplicate_option", "--since may be supplied only once.");
|
|
1398
|
+
const candidate = rest[index + 1];
|
|
1399
|
+
if (parseCliVersion(candidate) === undefined) {
|
|
1400
|
+
throw usageError(
|
|
1401
|
+
"changelog.since_invalid",
|
|
1402
|
+
"--since requires a version such as 0.20.0 (the version you upgraded from).",
|
|
1403
|
+
{ field: "since" },
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
since = candidate;
|
|
1407
|
+
index += 1;
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1317
1410
|
if (verb.dryRun && argument === "--dry-run") {
|
|
1318
1411
|
if (dryRun) throw usageError("cli.duplicate_option", "--dry-run may be supplied only once.");
|
|
1319
1412
|
dryRun = true;
|
|
@@ -1394,6 +1487,16 @@ function parseArguments(arguments_) {
|
|
|
1394
1487
|
`${verb.tokens.join(" ")} requires exactly one of --staging or --production.`,
|
|
1395
1488
|
);
|
|
1396
1489
|
}
|
|
1490
|
+
if (verb.handoffOptions && reviewPath !== undefined && handoff !== true) {
|
|
1491
|
+
throw usageError(
|
|
1492
|
+
"cli.review_path_requires_handoff",
|
|
1493
|
+
"--review-path chooses the page a --handoff review URL opens on; pass --handoff with it, or use 'staging review --path'.",
|
|
1494
|
+
{ field: "reviewPath" },
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
if (handoff === true && verb.target && deployTarget === DEPLOY_TARGET_PRODUCTION) {
|
|
1498
|
+
throw usageError("cli.handoff_target", "--handoff applies only to deploy --staging.", { field: "handoff" });
|
|
1499
|
+
}
|
|
1397
1500
|
if (verb.positionals === "fixturePath" && (positionals.length > 1 || (init && positionals.length !== 1))) {
|
|
1398
1501
|
throw usageError(
|
|
1399
1502
|
"validate.fixture_path_invalid",
|
|
@@ -1507,6 +1610,9 @@ function parseArguments(arguments_) {
|
|
|
1507
1610
|
quiet,
|
|
1508
1611
|
deployTarget,
|
|
1509
1612
|
allowFailedPreview,
|
|
1613
|
+
...(handoff === undefined ? {} : { handoff }),
|
|
1614
|
+
...(reviewPath === undefined ? {} : { reviewPath }),
|
|
1615
|
+
...(since === undefined ? {} : { since }),
|
|
1510
1616
|
...(deliveryUrl === undefined ? {} : { deliveryUrl }),
|
|
1511
1617
|
...(propagationWaitSeconds === undefined ? {} : { propagationWaitSeconds }),
|
|
1512
1618
|
...(browser === undefined ? {} : { browser }),
|
|
@@ -1599,6 +1705,9 @@ export async function runCli({
|
|
|
1599
1705
|
configPath: parsed.configPath,
|
|
1600
1706
|
deployTarget: parsed.deployTarget,
|
|
1601
1707
|
allowFailedPreview: parsed.allowFailedPreview,
|
|
1708
|
+
...(parsed.handoff === undefined ? {} : { handoff: parsed.handoff }),
|
|
1709
|
+
...(parsed.reviewPath === undefined ? {} : { reviewPath: parsed.reviewPath }),
|
|
1710
|
+
...(parsed.since === undefined ? {} : { since: parsed.since }),
|
|
1602
1711
|
init: parsed.init,
|
|
1603
1712
|
quiet: parsed.quiet,
|
|
1604
1713
|
dryRun: parsed.dryRun,
|
package/src/constants.js
CHANGED
|
@@ -106,6 +106,12 @@ export const VERB_STAGING_REVIEW = "staging review";
|
|
|
106
106
|
export const VERB_SITES = "sites";
|
|
107
107
|
export const VERB_USE = "use";
|
|
108
108
|
export const VERB_WHOAMI = "whoami";
|
|
109
|
+
export const VERB_CHANGELOG = "changelog";
|
|
110
|
+
|
|
111
|
+
/** The command that lists what changed since a version, for the CLI to name after an upgrade (TR01225). */
|
|
112
|
+
export function changelogCommandSince(version) {
|
|
113
|
+
return `${CLI_BINARY_NAME} ${VERB_CHANGELOG} --since ${version}`;
|
|
114
|
+
}
|
|
109
115
|
export const VERB_PLAN = "plan";
|
|
110
116
|
export const VERB_APPLY = "apply";
|
|
111
117
|
export const VERB_PLACES_SEARCH = "places search";
|
|
@@ -256,6 +262,19 @@ export const LIMITS = Object.freeze({
|
|
|
256
262
|
deploymentMilliseconds: 30 * 60_000,
|
|
257
263
|
previewMilliseconds: 5 * 60_000,
|
|
258
264
|
pollIntervalMilliseconds: 2_000,
|
|
265
|
+
// How long after a staging deployment completes the edge can still serve the
|
|
266
|
+
// previous release (TR01236): up to a minute for Workers KV to propagate the
|
|
267
|
+
// route, plus the Worker's 30-second KV read cache and 30-second isolate cache
|
|
268
|
+
// (docs/operations/published-site-worker-runbook.md, "Route resolution
|
|
269
|
+
// freshness budget"). The wait is longer than the ceiling so one slow read does
|
|
270
|
+
// not turn a propagating release into a false warning.
|
|
271
|
+
stagingPropagationCeilingSeconds: 120,
|
|
272
|
+
stagingServedMilliseconds: 180_000,
|
|
273
|
+
stagingServedPollMilliseconds: 3_000,
|
|
274
|
+
// Two reads in a row, because each read lands on one cached copy of the route.
|
|
275
|
+
stagingServedConfirmations: 2,
|
|
276
|
+
// The review notice sits at the end of the page, so the whole document is read.
|
|
277
|
+
stagingServedPageBytes: 4 * 1024 * 1024,
|
|
259
278
|
requestAttempts: 4,
|
|
260
279
|
uploadAttempts: 4,
|
|
261
280
|
diagnosticScalars: 2_000,
|
package/src/errors.js
CHANGED
|
@@ -130,6 +130,8 @@ export class SiteAuthoringError extends Error {
|
|
|
130
130
|
this.previewDiagnostic = normalizePreviewDiagnostic(options.previewDiagnostic);
|
|
131
131
|
this.field = typeof options.field === "string" ? sanitizeDiagnostic(options.field, "") : undefined;
|
|
132
132
|
this.status = typeof options.status === "string" ? sanitizeDiagnostic(options.status, "") : undefined;
|
|
133
|
+
// The site check rule (shared/site-check-rules.json) this refusal comes from, when it comes from one.
|
|
134
|
+
this.rule = typeof options.rule === "string" && /^[a-z0-9_.]+$/u.test(options.rule) ? options.rule : undefined;
|
|
133
135
|
this.alternatives = Array.isArray(options.alternatives)
|
|
134
136
|
? options.alternatives
|
|
135
137
|
.filter((value) => typeof value === "string")
|
|
@@ -191,6 +193,7 @@ export class SiteAuthoringError extends Error {
|
|
|
191
193
|
...(typeof problem.file === "string" ? { file: sanitizeDiagnostic(problem.file, "") } : {}),
|
|
192
194
|
code: /^[a-z0-9_.-]+$/u.test(problem.code) ? problem.code : "site.failed",
|
|
193
195
|
...(typeof problem.field === "string" ? { field: sanitizeDiagnostic(problem.field, "") } : {}),
|
|
196
|
+
...(typeof problem.rule === "string" && /^[a-z0-9_.]+$/u.test(problem.rule) ? { rule: problem.rule } : {}),
|
|
194
197
|
message: sanitizeDiagnostic(problem.message),
|
|
195
198
|
}));
|
|
196
199
|
this.problemCount = list.length;
|
package/src/footer-contract.js
CHANGED
|
@@ -278,11 +278,13 @@ function richText(value, path, maximumLength, maximumParagraphs, knownPageResour
|
|
|
278
278
|
if (text.length === 0) continue;
|
|
279
279
|
totalLength += text.length;
|
|
280
280
|
totalRuns += 1;
|
|
281
|
+
// A style flag that is off is left out, so a run carries only what it sets and a hand-written
|
|
282
|
+
// file matches what pull writes (TR01232).
|
|
281
283
|
normalizedRuns.push({
|
|
282
284
|
text,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
285
|
+
...(boolean(run.bold, false, `${runPath}.bold`) ? { bold: true } : {}),
|
|
286
|
+
...(boolean(run.italic, false, `${runPath}.italic`) ? { italic: true } : {}),
|
|
287
|
+
...(boolean(run.underline, false, `${runPath}.underline`) ? { underline: true } : {}),
|
|
286
288
|
...(run.link === undefined || run.link === null
|
|
287
289
|
? {}
|
|
288
290
|
: { link: inlineLink(run.link, `${runPath}.link`, knownPageResourceIds, tolerateStoredValues) }),
|
package/src/index.js
CHANGED
package/src/output.js
CHANGED
|
@@ -3,12 +3,14 @@ import { constants as fsConstants } from "node:fs";
|
|
|
3
3
|
import { lstat, open } from "node:fs/promises";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
changelogCommandSince,
|
|
6
7
|
CLI_BINARY_NAME,
|
|
7
8
|
CLI_NAME,
|
|
8
9
|
CLI_VERSION,
|
|
9
10
|
GITHUB_OUTPUT_KEY_PREFIX,
|
|
10
11
|
GITHUB_OUTPUT_RESULT_KEY,
|
|
11
12
|
LIMITS,
|
|
13
|
+
REFUSAL_CLI_OUTDATED,
|
|
12
14
|
REFUSAL_UNCLASSIFIED,
|
|
13
15
|
RESULT_SCHEMA_VERSION,
|
|
14
16
|
} from "./constants.js";
|
|
@@ -37,6 +39,7 @@ export function failureResult(error) {
|
|
|
37
39
|
error: { code: error.code },
|
|
38
40
|
};
|
|
39
41
|
if (error.field) result.error.field = error.field;
|
|
42
|
+
if (error.rule) result.error.rule = error.rule;
|
|
40
43
|
if (error.details?.length) result.error.details = error.details;
|
|
41
44
|
if (error.status) result.error.status = error.status;
|
|
42
45
|
if (Array.isArray(error.alternatives) && error.alternatives.length > 0) {
|
|
@@ -67,6 +70,12 @@ export function failureResult(error) {
|
|
|
67
70
|
if (previewRecovery) result.error.preview = previewRecovery;
|
|
68
71
|
const refusal = typeof error.refusalKind === "function" ? error.refusalKind() : undefined;
|
|
69
72
|
if (typeof refusal === "string" && refusal !== REFUSAL_UNCLASSIFIED) result.error.refusal = refusal;
|
|
73
|
+
if (refusal === REFUSAL_CLI_OUTDATED) {
|
|
74
|
+
const latestVersion = typeof error.latestCliVersion === "function" ? error.latestCliVersion() : error.latestVersion;
|
|
75
|
+
if (typeof latestVersion === "string") result.error.latestVersion = latestVersion;
|
|
76
|
+
// What to run once the upgrade is done, so the agent need not diff the help topics.
|
|
77
|
+
result.error.changelogCommand = changelogCommandSince(CLI_VERSION);
|
|
78
|
+
}
|
|
70
79
|
return result;
|
|
71
80
|
}
|
|
72
81
|
|
|
@@ -44,6 +44,12 @@ import {
|
|
|
44
44
|
REQUIRED_THEME_PROPERTIES,
|
|
45
45
|
validateAndLintThemePair,
|
|
46
46
|
} from "./theme-validation.js";
|
|
47
|
+
import {
|
|
48
|
+
describeSettingField,
|
|
49
|
+
formatSettingsReference,
|
|
50
|
+
PAGE_BACKGROUND_IMAGE_RENDERING,
|
|
51
|
+
PAGE_BACKGROUND_VERSUS_CANVAS,
|
|
52
|
+
} from "./setting-reference.js";
|
|
47
53
|
import {
|
|
48
54
|
SETTINGS_TYPE_SITE_PUBLISHING_PREFERENCES,
|
|
49
55
|
SETTINGS_TYPE_TAPROOT_STYLES,
|
|
@@ -273,14 +279,23 @@ const THEME_GROUPS = Object.freeze([
|
|
|
273
279
|
}),
|
|
274
280
|
Object.freeze({
|
|
275
281
|
name: "surface-assets",
|
|
276
|
-
summary:
|
|
282
|
+
summary:
|
|
283
|
+
"Preserve pulled asset fields; use appearance image ids for the logos, favicon and canvas image. The page "
|
|
284
|
+
+ "background image is a theme field behind the page content, unlike the canvas image, which paints only the "
|
|
285
|
+
+ "side margins (help settings has both).",
|
|
277
286
|
fields: Object.freeze([
|
|
278
287
|
themeField("stylesheets", "Agent-authored site themes must keep this exact array empty.", { exact: [] }),
|
|
279
288
|
themeField(
|
|
280
289
|
"pageBackgroundImage",
|
|
281
|
-
"CSS background-image
|
|
290
|
+
"CSS background-image (the app's Page style > Background image). " + PAGE_BACKGROUND_IMAGE_RENDERING
|
|
291
|
+
+ " Today it is a CSS url(...) of a delivered image, the src that .taproot-site-media.json records; it tiles "
|
|
292
|
+
+ "at the image's natural pixel size from the top-left. The complete Taproot workspace includes an empty value.",
|
|
293
|
+
),
|
|
294
|
+
themeField(
|
|
295
|
+
"pageBackgroundImageOpacity",
|
|
296
|
+
"How strongly the page background image shows, from 0 (invisible) to 1 (fully opaque).",
|
|
297
|
+
{ minimum: 0, maximum: 1 },
|
|
282
298
|
),
|
|
283
|
-
themeField("pageBackgroundImageOpacity", "Page background opacity.", { minimum: 0, maximum: 1 }),
|
|
284
299
|
themeField("boxBackgroundImage", "Optional inherited box-surface background image."),
|
|
285
300
|
themeField("boxBackgroundImageOpacity", "Optional box background opacity.", { minimum: 0, maximum: 1 }),
|
|
286
301
|
themeField("vellumOpacity", "Optional modal-vellum opacity.", { minimum: 0, maximum: 1 }),
|
|
@@ -418,6 +433,9 @@ export function getThemeReference() {
|
|
|
418
433
|
};
|
|
419
434
|
}
|
|
420
435
|
|
|
436
|
+
// "linkHoverColor" reads "link hover color", so a new footer color token needs no second table.
|
|
437
|
+
const footerColorLabel = (name) => name.replace(/([A-Z])/gu, " $1").toLowerCase();
|
|
438
|
+
|
|
421
439
|
function appearanceColorReference(definition) {
|
|
422
440
|
return definition.type === "appearance-color"
|
|
423
441
|
? {
|
|
@@ -432,6 +450,7 @@ export function getAppearanceReference() {
|
|
|
432
450
|
const scalarFields = APPEARANCE_SCALAR_FIELDS.map((field) => ({
|
|
433
451
|
file: APPEARANCE_FILES[field.settingsType],
|
|
434
452
|
path: "settings." + field.name,
|
|
453
|
+
...describeSettingField(field.settingsType, field.name),
|
|
435
454
|
type: field.type,
|
|
436
455
|
...(field.values ? { values: field.values } : {}),
|
|
437
456
|
...(field.minimum !== undefined ? { minimum: field.minimum, maximum: field.maximum } : {}),
|
|
@@ -447,27 +466,36 @@ export function getAppearanceReference() {
|
|
|
447
466
|
{
|
|
448
467
|
file: APPEARANCE_FILES[SETTINGS_TYPE_TAPROOT_STYLES],
|
|
449
468
|
path: "settings.lightTheme",
|
|
469
|
+
...describeSettingField(SETTINGS_TYPE_TAPROOT_STYLES, "lightTheme"),
|
|
450
470
|
type: "complete-espalier-theme",
|
|
451
471
|
},
|
|
452
472
|
{
|
|
453
473
|
file: APPEARANCE_FILES[SETTINGS_TYPE_TAPROOT_STYLES],
|
|
454
474
|
path: "settings.darkTheme",
|
|
475
|
+
...describeSettingField(SETTINGS_TYPE_TAPROOT_STYLES, "darkTheme"),
|
|
455
476
|
type: "complete-espalier-theme",
|
|
456
477
|
},
|
|
457
478
|
...scalarFields,
|
|
458
479
|
...APPEARANCE_FOOTER_COLOR_FIELDS.map((field) => ({
|
|
459
480
|
file: field.file,
|
|
460
481
|
path: field.path,
|
|
482
|
+
appLabel: "Footer",
|
|
483
|
+
description: `The footer's ${footerColorLabel(field.name)} in the ${field.scheme} scheme. footer push owns the rest of the footer.`,
|
|
484
|
+
renders: "The footer, once footerSettings.enabled is true (default false).",
|
|
461
485
|
type: field.type,
|
|
462
486
|
...appearanceColorReference(field),
|
|
463
487
|
default: field.default,
|
|
464
488
|
})),
|
|
465
489
|
],
|
|
466
490
|
readOnlyProjections: APPEARANCE_READ_ONLY_FIELDS,
|
|
491
|
+
pageBackgroundVersusCanvas: PAGE_BACKGROUND_VERSUS_CANVAS,
|
|
492
|
+
fieldMap: CLI_BINARY_NAME + " help settings maps every pulled field, including the read-only ones, to its app control.",
|
|
467
493
|
imageReferences:
|
|
468
494
|
"Use site-owned image ids retained from pull or returned by media upload. URLs are server projections, never authored inputs.",
|
|
469
495
|
logoContract:
|
|
470
496
|
"lightLogoId and darkLogoId are the current scheme-specific logo fields; there is no separate compact-logo setting. "
|
|
497
|
+
+ "Each takes one image id: upload the highest-resolution file you have and assign it. An @2x or @3x suffix is only "
|
|
498
|
+
+ "part of a filename, nothing pairs it with another file, and the platform generates the sizes. "
|
|
471
499
|
+ "Prefer transparent PNG or WebP artwork with genuine alpha so the header background shows through, including "
|
|
472
500
|
+ "inside letter counters. Avoid baked-in background rectangles unless the brand intentionally uses a badge. "
|
|
473
501
|
+ "Keep padding tight and verify the delivered image preserves transparency. Logo style and coloration are "
|
|
@@ -776,11 +804,17 @@ export function formatPresentationReference(reference) {
|
|
|
776
804
|
}
|
|
777
805
|
if (reference.referenceKind === "appearance") {
|
|
778
806
|
return reference.title + "\nUsage: " + reference.usage + "\n\nWritable fields:\n"
|
|
779
|
-
+ reference.fields.map((field) =>
|
|
780
|
-
"
|
|
781
|
-
|
|
807
|
+
+ reference.fields.map((field) =>
|
|
808
|
+
" " + field.file + " :: " + field.path + " — " + fieldConstraint(field)
|
|
809
|
+
+ "\n app: " + field.appLabel + ". " + field.description
|
|
810
|
+
+ (field.renders === undefined ? "" : "\n renders: " + field.renders)
|
|
811
|
+
).join("\n")
|
|
782
812
|
+ "\n\nRead-only server projections:\n"
|
|
783
813
|
+ reference.readOnlyProjections.map((field) => " " + field.file + " :: " + field.path).join("\n")
|
|
814
|
+
+ "\n\nPage background image versus canvas image:\n - " + reference.pageBackgroundVersusCanvas.pageBackground
|
|
815
|
+
+ "\n - " + reference.pageBackgroundVersusCanvas.canvas
|
|
816
|
+
+ "\n - " + reference.pageBackgroundVersusCanvas.shared
|
|
817
|
+
+ "\n\nField map: " + reference.fieldMap
|
|
784
818
|
+ "\n\nImages: " + reference.imageReferences + "\nLogos: " + reference.logoContract
|
|
785
819
|
+ "\nLogo contrast: " + reference.logoContrastContract
|
|
786
820
|
+ "\nFavicon: " + reference.faviconContract
|
|
@@ -794,6 +828,7 @@ export function formatPresentationReference(reference) {
|
|
|
794
828
|
+ "\nRetry: " + reference.retry
|
|
795
829
|
+ "\nRecovery: " + reference.recovery + "\n";
|
|
796
830
|
}
|
|
831
|
+
if (reference.referenceKind === "settings") return formatSettingsReference(reference);
|
|
797
832
|
if (reference.referenceKind === "footer") {
|
|
798
833
|
const messages = reference.messages.map((message) =>
|
|
799
834
|
message.name + " (unlisted fields rejected)\n"
|
package/src/problems.js
CHANGED
|
@@ -39,7 +39,12 @@ function withContext(problem, context, error) {
|
|
|
39
39
|
export function problemsOf(error, context = {}) {
|
|
40
40
|
const own = Array.isArray(error.problems) && error.problems.length > 0
|
|
41
41
|
? error.problems
|
|
42
|
-
: [{
|
|
42
|
+
: [{
|
|
43
|
+
code: error.code,
|
|
44
|
+
...(error.field === undefined ? {} : { field: error.field }),
|
|
45
|
+
...(error.rule === undefined ? {} : { rule: error.rule }),
|
|
46
|
+
message: error.message,
|
|
47
|
+
}];
|
|
43
48
|
return own.map((problem) => withContext(problem, context, error));
|
|
44
49
|
}
|
|
45
50
|
|