@taprootio/site-authoring 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +239 -0
  3. package/bin/taproot-site.js +5 -0
  4. package/package.json +40 -0
  5. package/src/api.js +1507 -0
  6. package/src/appearance-contract.js +311 -0
  7. package/src/atomic-file.js +136 -0
  8. package/src/bounded-file.js +59 -0
  9. package/src/capabilities.js +29 -0
  10. package/src/cli.js +872 -0
  11. package/src/config.js +412 -0
  12. package/src/constants.js +173 -0
  13. package/src/content/components.js +870 -0
  14. package/src/content/free-form-section-registry.json +524 -0
  15. package/src/content/free-form-sections.js +582 -0
  16. package/src/content/index.js +29 -0
  17. package/src/content/inline-facts.js +97 -0
  18. package/src/content/markdown.js +1625 -0
  19. package/src/content/tiptap-prosemirror.ts +1266 -0
  20. package/src/content/validate-document.js +1079 -0
  21. package/src/content/vocabulary.js +397 -0
  22. package/src/credentials.js +544 -0
  23. package/src/errors.js +133 -0
  24. package/src/footer-contract.js +687 -0
  25. package/src/footer-draft-hash.js +203 -0
  26. package/src/footer-workspace.js +136 -0
  27. package/src/image-metadata.js +169 -0
  28. package/src/index.js +16 -0
  29. package/src/json-guard.js +111 -0
  30. package/src/output.js +157 -0
  31. package/src/presentation-reference.js +600 -0
  32. package/src/reference-help.js +927 -0
  33. package/src/session.js +316 -0
  34. package/src/settings-catalog.js +187 -0
  35. package/src/settings.js +157 -0
  36. package/src/theme-validation.js +344 -0
  37. package/src/transport.js +777 -0
  38. package/src/verbs/approve.js +178 -0
  39. package/src/verbs/deploy.js +327 -0
  40. package/src/verbs/env.js +89 -0
  41. package/src/verbs/footer-push.js +115 -0
  42. package/src/verbs/index.js +125 -0
  43. package/src/verbs/login.js +529 -0
  44. package/src/verbs/logout.js +39 -0
  45. package/src/verbs/media-upload.js +319 -0
  46. package/src/verbs/nav-push.js +333 -0
  47. package/src/verbs/pages-push.js +990 -0
  48. package/src/verbs/preview-page.js +107 -0
  49. package/src/verbs/preview-revoke.js +56 -0
  50. package/src/verbs/pull.js +727 -0
  51. package/src/verbs/sites.js +57 -0
  52. package/src/verbs/status.js +94 -0
  53. package/src/verbs/theme-push.js +197 -0
  54. package/src/verbs/use.js +129 -0
  55. package/src/verbs/validate.js +477 -0
  56. package/src/verbs/whoami.js +108 -0
  57. package/src/workspace.js +1085 -0
@@ -0,0 +1,115 @@
1
+ import { saveSiteFooterSettings, withRefusalGuidance } from "../api.js";
2
+ import { VERB_FOOTER_PUSH } from "../constants.js";
3
+ import { SiteAuthoringError } from "../errors.js";
4
+ import { projectFooterSettingsForWorkspace, validateFooterDocument } from "../footer-contract.js";
5
+ import { computeFooterDraftHash } from "../footer-draft-hash.js";
6
+ import { advanceFooterManifest, FOOTER_SETTINGS_FILE, readFooterWorkspaceContext } from "../footer-workspace.js";
7
+ import { openSession, successResult } from "../session.js";
8
+ import { SETTINGS_TYPE_SITE_PUBLISHING_PREFERENCES } from "../settings-catalog.js";
9
+ import { ApiError } from "../transport.js";
10
+ import { readWorkspaceJson, WORKSPACE_LIMITS, writeWorkspaceJson } from "../workspace.js";
11
+
12
+ function isPlainObject(value) {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value);
14
+ }
15
+
16
+ function requireSettingsDocument(document_, siteId, expectedEntityId = siteId) {
17
+ if (!isPlainObject(document_) || !isPlainObject(document_.settings)) {
18
+ throw new SiteAuthoringError(
19
+ "footer.settings_invalid",
20
+ `${FOOTER_SETTINGS_FILE} must be the settings document written by pull.`,
21
+ { field: FOOTER_SETTINGS_FILE },
22
+ );
23
+ }
24
+ if (
25
+ document_.entityId !== expectedEntityId
26
+ || document_.settingsType !== SETTINGS_TYPE_SITE_PUBLISHING_PREFERENCES
27
+ ) {
28
+ throw new SiteAuthoringError(
29
+ "footer.settings_site_mismatch",
30
+ `${FOOTER_SETTINGS_FILE} is not bound to this site. Run 'taproot-site pull' again.`,
31
+ { field: FOOTER_SETTINGS_FILE },
32
+ );
33
+ }
34
+ return document_;
35
+ }
36
+
37
+ /** The complete local validation phase shared by live push and offline fixtures. */
38
+ export function validateFooterWorkspaceDocument(document_, siteId, options = {}) {
39
+ const { expectedEntityId = siteId, ...validationOptions } = options;
40
+ const settingsDocument = requireSettingsDocument(document_, siteId, expectedEntityId);
41
+ return {
42
+ document: settingsDocument,
43
+ footerSettings: validateFooterDocument(settingsDocument.settings.footerSettings, validationOptions),
44
+ };
45
+ }
46
+
47
+ function translateConcurrency(error) {
48
+ if (error instanceof ApiError && error.hasField("ExpectedFooterDraftHash")) {
49
+ return new SiteAuthoringError(
50
+ "footer.concurrent_modification",
51
+ "The footer changed after this workspace was pulled. Run 'taproot-site pull', reconcile the footer document, and retry.",
52
+ { field: "expectedFooterDraftHash", status: error.status },
53
+ );
54
+ }
55
+ return error;
56
+ }
57
+
58
+ export async function footerPush(invocation) {
59
+ const { client, config, siteId, onProgress } = await openSession(invocation);
60
+ const context = await readFooterWorkspaceContext(config.workspaceDir, siteId);
61
+ const { document: document_, footerSettings } = validateFooterWorkspaceDocument(
62
+ await readWorkspaceJson(config.workspaceDir, FOOTER_SETTINGS_FILE, WORKSPACE_LIMITS.settingsBytes),
63
+ siteId,
64
+ {
65
+ knownPageResourceIds: context.knownPageResourceIds,
66
+ knownImageIds: context.knownImageIds,
67
+ },
68
+ );
69
+ onProgress("Validated the complete footer document, stable page targets, and site-owned image references locally.");
70
+
71
+ let response;
72
+ try {
73
+ response = await withRefusalGuidance(onProgress, "footer push", async () =>
74
+ await saveSiteFooterSettings(
75
+ client,
76
+ siteId,
77
+ footerSettings,
78
+ context.footer.expectedDraftHash,
79
+ ));
80
+ } catch (error) {
81
+ throw translateConcurrency(error);
82
+ }
83
+
84
+ if (!isPlainObject(response.footerSettings)) {
85
+ throw new SiteAuthoringError(
86
+ "api.footer_settings_contract",
87
+ "Taproot returned no normalized footer document after saving.",
88
+ { field: "footerSettings" },
89
+ ).withCompletedWrites(["footerSettings"]);
90
+ }
91
+
92
+ let saved;
93
+ let footerDraftHash;
94
+ try {
95
+ saved = projectFooterSettingsForWorkspace(response.footerSettings);
96
+ footerDraftHash = computeFooterDraftHash(saved);
97
+ const nextDocument = structuredClone(document_);
98
+ nextDocument.settings.footerSettings = saved;
99
+ // The authorable document lands before its token. If either local write
100
+ // fails, a stale token prevents the next push from overwriting the saved
101
+ // remote footer and the completed-write report tells the caller to pull.
102
+ await writeWorkspaceJson(config.workspaceDir, FOOTER_SETTINGS_FILE, nextDocument);
103
+ await advanceFooterManifest(config.workspaceDir, siteId, saved, footerDraftHash);
104
+ } catch (error) {
105
+ if (error instanceof SiteAuthoringError) throw error.withCompletedWrites(["footerSettings"]);
106
+ throw error;
107
+ }
108
+ onProgress("Saved the complete footer and refreshed its local draft hash.");
109
+
110
+ return successResult(VERB_FOOTER_PUSH, siteId, {
111
+ footerFile: FOOTER_SETTINGS_FILE,
112
+ footerDraftHash,
113
+ written: { items: ["footerSettings"], count: 1 },
114
+ });
115
+ }
@@ -0,0 +1,125 @@
1
+ import {
2
+ VERB_APPROVE,
3
+ VERB_DEPLOY,
4
+ VERB_ENV,
5
+ VERB_FOOTER_PUSH,
6
+ VERB_LOGIN,
7
+ VERB_LOGOUT,
8
+ VERB_MEDIA_UPLOAD,
9
+ VERB_NAV_PUSH,
10
+ VERB_PAGES_PUSH,
11
+ VERB_PULL,
12
+ VERB_PREVIEW_PAGE,
13
+ VERB_PREVIEW_REVOKE,
14
+ VERB_SITES,
15
+ VERB_STATUS,
16
+ VERB_THEME_PUSH,
17
+ VERB_USE,
18
+ VERB_VALIDATE,
19
+ VERB_WHOAMI,
20
+ } from "../constants.js";
21
+ import { approve } from "./approve.js";
22
+ import { deploy } from "./deploy.js";
23
+ import { env } from "./env.js";
24
+ import { footerPush } from "./footer-push.js";
25
+ import { login } from "./login.js";
26
+ import { logout } from "./logout.js";
27
+ import { mediaUpload } from "./media-upload.js";
28
+ import { navPush } from "./nav-push.js";
29
+ import { pagesPush } from "./pages-push.js";
30
+ import { pull } from "./pull.js";
31
+ import { previewPage } from "./preview-page.js";
32
+ import { previewRevoke } from "./preview-revoke.js";
33
+ import { sites } from "./sites.js";
34
+ import { status } from "./status.js";
35
+ import { themePush } from "./theme-push.js";
36
+ import { use } from "./use.js";
37
+ import { validateFixture } from "./validate.js";
38
+ import { whoami } from "./whoami.js";
39
+
40
+ /**
41
+ * The verb seam.
42
+ *
43
+ * `cli.js` parses the command line, decides nothing about what a verb means,
44
+ * and calls exactly one handler from this table with a single frozen
45
+ * invocation object:
46
+ *
47
+ * ```
48
+ * {
49
+ * verb, // the canonical verb name, e.g. "pages push"
50
+ * cwd, // directory configuration discovery starts from
51
+ * environment, // the process environment: TAPROOT_SITE_KEY, and the HOME /
52
+ * // XDG_CONFIG_HOME that locate the stored credential
53
+ * configPath, // an explicit --config path, or undefined
54
+ * deployTarget, // "staging" | "production" for `deploy`, else undefined
55
+ * quiet, // --quiet; suppresses progress, never the JSON result
56
+ * allowRawHtml, // --allow-raw-html; `pages push` only, false everywhere else
57
+ * keyName, // --name; `login` only, undefined everywhere else
58
+ * onProgress, // human progress sink (stderr, or a no-op when quiet)
59
+ * fetch, // injectable fetch, for tests
60
+ * signal, // optional AbortSignal
61
+ * }
62
+ * ```
63
+ *
64
+ * Two verbs are credential-free but not config-free. `login` and `logout`
65
+ * manage the stored credential rather than using one, so they read the
66
+ * configuration (for the site id and the reviewed API origin) and never require
67
+ * `TAPROOT_SITE_KEY`.
68
+ *
69
+ * Verbs that take positional arguments receive them under their own
70
+ * seam name, and only when some were given — a bare invocation keeps each
71
+ * verb's documented default:
72
+ *
73
+ * ```
74
+ * {
75
+ * fixturePath, // validate: one offline authoring-fixture directory
76
+ * paths, // media upload: files and/or directories to upload;
77
+ * // with none, the workspace's media/ directory is walked
78
+ * pagePaths, // approve: narrow to these page paths; with none, every
79
+ * // draft the workspace manifest tracks is staged
80
+ * pageId, // preview page: the one persisted draft UUID to render
81
+ * previewIds, // preview revoke: [page UUID, snapshot UUID]
82
+ * }
83
+ * ```
84
+ *
85
+ * A handler resolves with the machine-readable result object that reaches
86
+ * stdout, or throws a `SiteAuthoringError`.
87
+ *
88
+ * Handlers also accept a few optional keys `cli.js` does not set. They are the
89
+ * seams tests inject through, and the shape a programmatic caller fills in:
90
+ *
91
+ * ```
92
+ * {
93
+ * client, // a prebuilt SiteApiClient, bypassing construction
94
+ * sleep, now, // deterministic clocks for the bounded polls
95
+ * content, // the src/content/ module (pages push)
96
+ * stagedPageIds, // deploy --staging: an explicit candidate; refused
97
+ * selectedSettingsTypes, // outright for --production, which promotes a
98
+ * includeNavigation, // staging deployment and nothing else
99
+ * stagingDeploymentId, // deploy --production: promote this one
100
+ * }
101
+ * ```
102
+ *
103
+ * Every optional key defaults to the safe reading when absent: no raw HTML, the
104
+ * workspace's own directories, and the whole approved candidate.
105
+ */
106
+ export const VERB_HANDLERS = Object.freeze({
107
+ [VERB_VALIDATE]: validateFixture,
108
+ [VERB_LOGIN]: login,
109
+ [VERB_LOGOUT]: logout,
110
+ [VERB_PULL]: pull,
111
+ [VERB_PAGES_PUSH]: pagesPush,
112
+ [VERB_NAV_PUSH]: navPush,
113
+ [VERB_THEME_PUSH]: themePush,
114
+ [VERB_FOOTER_PUSH]: footerPush,
115
+ [VERB_MEDIA_UPLOAD]: mediaUpload,
116
+ [VERB_APPROVE]: approve,
117
+ [VERB_DEPLOY]: deploy,
118
+ [VERB_PREVIEW_PAGE]: previewPage,
119
+ [VERB_PREVIEW_REVOKE]: previewRevoke,
120
+ [VERB_SITES]: sites,
121
+ [VERB_USE]: use,
122
+ [VERB_WHOAMI]: whoami,
123
+ [VERB_ENV]: env,
124
+ [VERB_STATUS]: status,
125
+ });