@shoalrun/cli 0.1.0-alpha.12 → 0.1.0-alpha.14

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 (193) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +45 -5
  3. package/agent-docs/.well-known/shoalrun/navigation/project-updates-50f26fe22670.txt +3 -1
  4. package/agent-docs/SNAPSHOT.json +5 -5
  5. package/agent-docs/api-docs/support.md +11 -10
  6. package/agent-docs/author/components.md +13 -1
  7. package/agent-docs/author/frontmatter-reference.md +28 -25
  8. package/agent-docs/author/frontmatter-reference.sections/choose-a-publication-and-visibility.md +3 -1
  9. package/agent-docs/author/frontmatter-reference.sections/control-output-details.md +3 -1
  10. package/agent-docs/author/frontmatter-reference.sections/global-defaults-and-enforced-values.md +3 -1
  11. package/agent-docs/author/frontmatter-reference.sections/html-markdown-and-agents.md +3 -1
  12. package/agent-docs/author/frontmatter-reference.sections/identify-and-route-a-page.md +3 -1
  13. package/agent-docs/author/frontmatter-reference.sections/point-agents-to-it.md +3 -1
  14. package/agent-docs/author/frontmatter-reference.sections/project-defined-fields.md +3 -1
  15. package/agent-docs/author/frontmatter-reference.sections/related-reading.md +3 -1
  16. package/agent-docs/author/frontmatter-reference.sections/use-the-local-files-while-authoring.md +3 -1
  17. package/agent-docs/author/frontmatter-reference.sections/when-the-files-update.md +3 -1
  18. package/agent-docs/author/frontmatter.md +5 -2
  19. package/agent-docs/author/frontmatter.sections/audience-badges.md +1 -1
  20. package/agent-docs/author/frontmatter.sections/give-deprecation-guidance.md +1 -1
  21. package/agent-docs/author/frontmatter.sections/global-defaults-and-enforced-values.md +1 -1
  22. package/agent-docs/author/frontmatter.sections/optional-native-fields.md +5 -2
  23. package/agent-docs/author/frontmatter.sections/publication-fields.md +1 -1
  24. package/agent-docs/author/frontmatter.sections/publishing-policy.md +1 -1
  25. package/agent-docs/author/frontmatter.sections/related-metadata-tasks.md +1 -1
  26. package/agent-docs/author/frontmatter.sections/related-reading.md +1 -1
  27. package/agent-docs/author/frontmatter.sections/required-after-inheritance.md +1 -1
  28. package/agent-docs/author/frontmatter.sections/seo-fields.md +1 -1
  29. package/agent-docs/author/frontmatter.sections/validation-rules.md +1 -1
  30. package/agent-docs/author/markdown-metadata.md +13 -5
  31. package/agent-docs/author/navigation.md +12 -1
  32. package/agent-docs/author/navigation.sections/arrange-authored-pages.md +1 -1
  33. package/agent-docs/author/navigation.sections/fill-an-api-tag-group.md +1 -1
  34. package/agent-docs/author/navigation.sections/hide-group-page-counts.md +12 -1
  35. package/agent-docs/author/navigation.sections/html-markdown-and-agents.md +1 -1
  36. package/agent-docs/author/navigation.sections/local-api-preview.md +1 -1
  37. package/agent-docs/author/navigation.sections/mix-guides-and-api-references.md +1 -1
  38. package/agent-docs/author/navigation.sections/optionally-split-a-large-menu-into-files.md +1 -1
  39. package/agent-docs/author/navigation.sections/reach-navigation-while-reading.md +1 -1
  40. package/agent-docs/author/navigation.sections/related-reading.md +1 -1
  41. package/agent-docs/author/navigation.sections/split-navigation-into-tabs.md +1 -1
  42. package/agent-docs/author/navigation.sections/subject-lifecycle-labels.md +1 -1
  43. package/agent-docs/author/templates.md +10 -2
  44. package/agent-docs/author/templates.sections/built-in-feature-templates.md +1 -1
  45. package/agent-docs/author/templates.sections/built-in-templates.md +1 -1
  46. package/agent-docs/author/templates.sections/choose-formatting-enforcement-for-your-project.md +1 -1
  47. package/agent-docs/author/templates.sections/create-or-edit-a-project-template.md +10 -2
  48. package/agent-docs/author/templates.sections/keep-the-four-shapes-distinct.md +1 -1
  49. package/agent-docs/author/templates.sections/project-added-templates.md +1 -1
  50. package/agent-docs/author/templates.sections/related-reading.md +1 -1
  51. package/agent-docs/author/templates.sections/scaffold-a-page.md +1 -1
  52. package/agent-docs/author/templates.sections/short-cli-and-derived-values.md +1 -1
  53. package/agent-docs/author/templates.sections/what-every-template-gives-you.md +1 -1
  54. package/agent-docs/author/workspace.md +7 -1
  55. package/agent-docs/author/writing-style.md +13 -1
  56. package/agent-docs/author/writing-style.sections/adapt-this-example.md +1 -1
  57. package/agent-docs/author/writing-style.sections/choose-components-by-reader-need.md +1 -1
  58. package/agent-docs/author/writing-style.sections/evidence-and-uncertainty.md +1 -1
  59. package/agent-docs/author/writing-style.sections/example-audience-and-scope.md +1 -1
  60. package/agent-docs/author/writing-style.sections/feature-availability.md +36 -0
  61. package/agent-docs/author/writing-style.sections/ingredients-recipes-and-cookbooks.md +1 -1
  62. package/agent-docs/author/writing-style.sections/page-purpose-and-structure.md +1 -1
  63. package/agent-docs/author/writing-style.sections/procedures-and-examples.md +1 -1
  64. package/agent-docs/author/writing-style.sections/related-reading.md +1 -1
  65. package/agent-docs/author/writing-style.sections/review-a-draft.md +1 -1
  66. package/agent-docs/author/writing-style.sections/reviewed-writing-examples.md +1 -1
  67. package/agent-docs/author/writing-style.sections/voice-and-explanations.md +1 -1
  68. package/agent-docs/changelog/2026-10-28.md +89 -0
  69. package/agent-docs/changelog.md +9 -1
  70. package/agent-docs/config/outputs.md +15 -11
  71. package/agent-docs/config/outputs.sections/exports.md +14 -11
  72. package/agent-docs/config/outputs.sections/metadata.md +1 -1
  73. package/agent-docs/config/outputs.sections/related-reading.md +1 -1
  74. package/agent-docs/content-delivery/agents.md +86 -0
  75. package/agent-docs/content-delivery/api.md +130 -0
  76. package/agent-docs/content-delivery/client.md +79 -0
  77. package/agent-docs/content-delivery/http-api/operations/get-content-representation-fa1d2e3f.md +394 -0
  78. package/agent-docs/content-delivery/http-api/operations/get-content-representation-head-4052375c.md +256 -0
  79. package/agent-docs/content-delivery/http-api/operations/get-content-resource-849f1c49.md +911 -0
  80. package/agent-docs/content-delivery/http-api/operations/get-content-resource-head-0a352acd.md +246 -0
  81. package/agent-docs/content-delivery/http-api/operations/list-content-resources-8e977ec7.md +913 -0
  82. package/agent-docs/content-delivery/http-api/operations/list-content-resources-head-b67cbad9.md +236 -0
  83. package/agent-docs/content-delivery/http-api.md +78 -0
  84. package/agent-docs/content-delivery/manifest.md +79 -0
  85. package/agent-docs/content-delivery/publish.md +72 -0
  86. package/agent-docs/content-delivery/render.md +73 -0
  87. package/agent-docs/content-delivery/rest.md +108 -0
  88. package/agent-docs/content-delivery/sections.md +110 -0
  89. package/agent-docs/content-delivery/select.md +100 -0
  90. package/agent-docs/content-delivery.md +79 -0
  91. package/agent-docs/documents.json +6748 -5073
  92. package/agent-docs/provenance.json +4 -4
  93. package/agent-docs/publish/content-export.md +32 -16
  94. package/agent-docs/publish/content-export.sections/assets-downloads-and-notices.md +3 -3
  95. package/agent-docs/publish/content-export.sections/check-the-result.md +2 -2
  96. package/agent-docs/publish/content-export.sections/export-selected-pages.md +12 -9
  97. package/agent-docs/publish/content-export.sections/inspect-the-manifest.md +5 -3
  98. package/agent-docs/publish/content-export.sections/prerequisites.md +3 -3
  99. package/agent-docs/publish/content-export.sections/related-reading.md +2 -2
  100. package/agent-docs/publish/content-export.sections/review-exported-api-material.md +3 -3
  101. package/agent-docs/publish/content-export.sections/try-the-help-drawer-example.md +3 -3
  102. package/agent-docs/publish/content-export.sections/upgrade-an-existing-integration.md +34 -0
  103. package/agent-docs/publish/content-export.sections/what-the-representations-retain.md +4 -4
  104. package/agent-docs/publish/mcp.md +3 -1
  105. package/agent-docs/publish/mcp.sections/check-budgets-and-errors.md +1 -1
  106. package/agent-docs/publish/mcp.sections/choose-the-documentation-source.md +1 -1
  107. package/agent-docs/publish/mcp.sections/configure-the-client.md +1 -1
  108. package/agent-docs/publish/mcp.sections/generate-a-vercel-endpoint.md +1 -1
  109. package/agent-docs/publish/mcp.sections/related-reading.md +3 -1
  110. package/agent-docs/publish/mcp.sections/search-then-read.md +1 -1
  111. package/agent-docs/publish/mcp.sections/serve-a-public-endpoint-on-node.md +1 -1
  112. package/agent-docs/publish/mcp.sections/verify-the-connection.md +1 -1
  113. package/agent-docs/publish/mcp.sections/why-offer-mcp-for-public-docs.md +1 -1
  114. package/agent-docs/publish/seo.md +16 -2
  115. package/agent-docs/publish/seo.sections/related-reading.md +28 -0
  116. package/agent-docs/publish/seo.sections/set-the-public-address.md +36 -0
  117. package/agent-docs/publish/seo.sections/what-agents-receive.md +28 -0
  118. package/agent-docs/publish/seo.sections/what-the-build-adds.md +42 -0
  119. package/agent-docs/publish/seo.sections/write-the-page-title-and-summary.md +43 -0
  120. package/agent-docs/search-index.json +1 -1
  121. package/agent-docs/styling/layouts.md +21 -3
  122. package/agent-docs/styling/layouts.sections/build-a-landing-page.md +6 -3
  123. package/agent-docs/styling/layouts.sections/build-a-splash-page.md +6 -3
  124. package/agent-docs/styling/layouts.sections/control-navigation-tabs.md +6 -3
  125. package/agent-docs/styling/layouts.sections/customize-the-hero.md +6 -3
  126. package/agent-docs/styling/layouts.sections/hero.md +6 -3
  127. package/agent-docs/styling/layouts.sections/hide-the-table-of-contents-or-give-content-more-room.md +54 -0
  128. package/agent-docs/styling/layouts.sections/html-markdown-and-agents.md +6 -3
  129. package/agent-docs/styling/layouts.sections/related-reading.md +6 -3
  130. package/dist/api.js +70 -3
  131. package/dist/authoring-hints.d.ts +3 -0
  132. package/dist/authoring-hints.js +35 -0
  133. package/dist/authoring-project.d.ts +1 -1
  134. package/dist/authoring-project.js +2 -0
  135. package/dist/authoring-validation.js +2 -0
  136. package/dist/code-samples.js +22 -11
  137. package/dist/compiler-config.d.ts +1 -0
  138. package/dist/compiler-config.js +13 -0
  139. package/dist/compiler.js +2 -1
  140. package/dist/component-names.js +1 -1
  141. package/dist/component-transform.js +7 -1
  142. package/dist/content-delivery-contract.d.ts +108 -0
  143. package/dist/content-delivery-contract.js +15 -0
  144. package/dist/content-delivery-transport.d.ts +3 -0
  145. package/dist/content-delivery-transport.js +47 -0
  146. package/dist/content-delivery-validation.d.ts +7 -0
  147. package/dist/content-delivery-validation.js +122 -0
  148. package/dist/content-delivery.d.ts +21 -0
  149. package/dist/content-delivery.js +119 -0
  150. package/dist/content-export-anchors.d.ts +11 -0
  151. package/dist/content-export-anchors.js +79 -0
  152. package/dist/content-export-command.d.ts +4 -2
  153. package/dist/content-export-command.js +15 -2
  154. package/dist/content-export-comparison.d.ts +16 -0
  155. package/dist/content-export-comparison.js +48 -0
  156. package/dist/content-export-config.d.ts +17 -0
  157. package/dist/content-export-config.js +47 -3
  158. package/dist/content-export-render.js +20 -1
  159. package/dist/content-export.d.ts +5 -1
  160. package/dist/content-export.js +193 -40
  161. package/dist/content-mount.d.ts +11 -0
  162. package/dist/content-mount.js +73 -0
  163. package/dist/content-rest-client.d.ts +12 -0
  164. package/dist/content-rest-client.js +162 -0
  165. package/dist/content-rest-contract.d.ts +27 -0
  166. package/dist/content-rest-contract.js +9 -0
  167. package/dist/content-rest-server.d.ts +7 -0
  168. package/dist/content-rest-server.js +99 -0
  169. package/dist/content-source.d.ts +3 -1
  170. package/dist/frontmatter.js +8 -7
  171. package/dist/html-renderer.js +25 -0
  172. package/dist/markdown-sections.d.ts +2 -1
  173. package/dist/markdown-sections.js +8 -18
  174. package/dist/page-renderer.js +5 -3
  175. package/dist/publication-plan.js +2 -1
  176. package/dist/scaffold-placeholders.d.ts +2 -0
  177. package/dist/scaffold-placeholders.js +19 -0
  178. package/dist/section-identity.d.ts +24 -0
  179. package/dist/section-identity.js +137 -0
  180. package/examples/content-delivery/README.md +87 -0
  181. package/examples/content-delivery/app.js +91 -0
  182. package/examples/content-delivery/content/billing.mdx +22 -0
  183. package/examples/content-delivery/docs.config.yaml +36 -0
  184. package/examples/content-delivery/index.html +25 -0
  185. package/examples/content-delivery/preview.mjs +86 -0
  186. package/examples/content-delivery/snippets/refund-window.mdx +3 -0
  187. package/examples/help-drawer/README.md +2 -2
  188. package/examples/help-drawer/app.js +3 -3
  189. package/package.json +6 -3
  190. package/schemas/content-export.schema.json +469 -33
  191. package/schemas/content-rest.openapi.json +1627 -0
  192. package/skills/shoalrun-author/SKILL.md +2 -0
  193. package/styles/site.css +9 -4
@@ -0,0 +1,87 @@
1
+ # Use pages, sections, and snippets in an application
2
+
3
+ This example uses the public `@shoalrun/cli/content` client to show a whole
4
+ billing guide, its independently approved refund section, and two simultaneous
5
+ copies of one reusable policy snippet. The app owns the drawer, buttons, focus,
6
+ and styling. Shoalrun supplies passive content and typed resource references.
7
+
8
+ Copy this directory into a project with Shoalrun installed. From the copied
9
+ example directory, run:
10
+
11
+ ```sh
12
+ npx --no-install shoalrun export docs.config.yaml --target app-help --out public/help/releases/demo
13
+ node preview.mjs
14
+ ```
15
+
16
+ Open `http://127.0.0.1:8775/settings/billing`. Choose **Billing guide** to read the
17
+ page or **How to request a refund** to read only its named section. Follow the
18
+ section's billing-overview link. Both cards display the same snippet, with
19
+ independent footnote anchors. Escape closes the drawer and restores focus.
20
+
21
+ The preview resolves the installed package's public `@shoalrun/cli/content`
22
+ entrypoint and serves only its browser-safe modules. It does not serve arbitrary
23
+ package or project files. In a real app, import that entrypoint through your
24
+ existing browser bundler.
25
+
26
+ ## Author the resources
27
+
28
+ The page gives its root heading a stable ID with
29
+ `## Request a refund <Anchor id="refunds" />`. Its page metadata separately
30
+ declares `{ id: refunds, context: section }` in `standaloneSections`, approving
31
+ that section as useful without the page introduction.
32
+
33
+ The export target selects a page reference, a section reference, and a named
34
+ snippet instance with a fixed `days` value. `snippets/refund-window.mdx` is also
35
+ included in the page, so the reusable wording stays in one source. Snippet
36
+ content is block content; this example does not insert it inside a button or
37
+ promise that arbitrary snippets are suitable for inline tooltips.
38
+
39
+ ## Keep serving independent from presentation
40
+
41
+ `app.js` opens a snapshot with `openManifestContentSource`, reads resources by
42
+ typed reference, and uses `mountContent` for the HTML. Each mount receives fresh
43
+ DOM IDs and remapped anchors. The drawer aborts superseded requests and ignores
44
+ late results after closing. These are application behaviors, not server duties.
45
+
46
+ The preview also serves the read-only REST API at `/content/v1`. Open
47
+ `http://127.0.0.1:8775/settings/billing?delivery=rest` to use
48
+ `openRestContentSource` through the same snapshot interface. Both modes deliver
49
+ identical content and use the same application rendering code.
50
+
51
+ The server uses `createContentRestHandler` from `@shoalrun/cli/content/server`.
52
+ It pins one approved snapshot per publication at startup and retains no revision
53
+ history. All requests select `channel`, `version`, and `language`. List resources
54
+ first, then use the returned key and revision for subsequent reads:
55
+
56
+ ```sh
57
+ curl 'http://127.0.0.1:8775/content/v1/resources?channel=production&version=current&language=en'
58
+ ```
59
+
60
+ `GET /resources/{key}` returns metadata; `GET
61
+ /resources/{key}/representations/{format}` returns HTML or Markdown. Append the
62
+ same publication parameters and `revision` returned by the list. HEAD and
63
+ conditional requests with `If-None-Match` are supported. The complete
64
+ [OpenAPI contract](../../schemas/content-rest.openapi.json) describes responses,
65
+ errors, and caching. The HTTP contract's `v1` is separate from package versions.
66
+
67
+ Keep `/help/releases/demo/` served alongside the API: the REST adapter preserves
68
+ exported bytes, including static links and asset URLs. Browser clients require
69
+ the application origin and HTTPS or localhost. This local example does not add
70
+ authentication, search, editing, or a production hosting service.
71
+
72
+ ## Publish complete revisions
73
+
74
+ The example uses `/help/releases/demo/` as a fixed demonstration revision.
75
+ Production should use a new directory and matching `publicBase` for each release,
76
+ upload the complete export, and only then update the app's manifest URL or
77
+ release pointer. Retain revisions still used by open clients. Do not overwrite
78
+ files underneath an existing snapshot.
79
+
80
+ Use `--previous-manifest PATH --fail-on-removed` to detect removed public resource
81
+ references before replacing output. Without a baseline, the export reports that
82
+ removal coverage was unavailable. API review candidates are not approved content:
83
+ the client refuses a manifest whose review status is `required`.
84
+
85
+ The content comes from this project's trusted build. The client checks the
86
+ manifest and content digests; these establish consistency with that manifest,
87
+ not trust in an arbitrary third-party publisher. Preserve the export's notices.
@@ -0,0 +1,91 @@
1
+ import { openManifestContentSource, openRestContentSource, mountContent } from '/shoalrun/content-delivery.js';
2
+
3
+ // The app owns interaction. A future server-backed source can supply this same
4
+ // snapshot interface; the drawer does not depend on manifest layout or filenames.
5
+ const lifetime = new AbortController();
6
+ const source = new URLSearchParams(location.search).get('delivery') === 'rest'
7
+ ? openRestContentSource({ baseUrl: '/content/v1', publication: { channel: 'production', version: 'current', language: 'en' }, signal: lifetime.signal })
8
+ : openManifestContentSource({ manifestUrl: '/help/releases/demo/manifest.json', signal: lifetime.signal });
9
+ const drawer = document.querySelector('#drawer');
10
+ const container = document.querySelector('#help-content');
11
+ let generation = 0;
12
+ let pending;
13
+ let mounted;
14
+ let opener;
15
+ let snapshot;
16
+ let disposed = false;
17
+ const notes = [];
18
+
19
+ async function openHelp(ref, fragment) {
20
+ if (disposed) return;
21
+ const ticket = ++generation;
22
+ pending?.abort();
23
+ const controller = new AbortController();
24
+ pending = controller;
25
+ try {
26
+ const content = await source;
27
+ if (ticket !== generation) return;
28
+ const result = await content.get(ref, { format: 'html', signal: controller.signal });
29
+ if (ticket !== generation) return;
30
+ mounted?.unmount();
31
+ mounted = mountContent(container, result);
32
+ drawer.hidden = false;
33
+ const anchor = fragment && mounted.anchors.find(item => item.id === fragment || item.exportedId === fragment);
34
+ const target = anchor ? container.querySelector(`#${CSS.escape(anchor.exportedId)}`) : container.querySelector('h1,h2,h3') ?? container;
35
+ // Stable aliases precede the visible heading. Focus the heading rather than
36
+ // an empty aria-hidden alias so keyboard and screen-reader users get context.
37
+ const focus = target?.getAttribute('aria-hidden') === 'true' && target.nextElementSibling?.matches('h1,h2,h3,h4,h5,h6')
38
+ ? target.nextElementSibling : target;
39
+ if (focus) { focus.tabIndex = -1; focus.focus({ preventScroll: true }); }
40
+ if (anchor) focus?.scrollIntoView();
41
+ else drawer.scrollTop = 0;
42
+ } catch (error) {
43
+ if (ticket !== generation || controller.signal.aborted) return;
44
+ mounted?.unmount();
45
+ mounted = undefined;
46
+ container.textContent = `Help could not be loaded: ${error.message}`;
47
+ drawer.hidden = false;
48
+ document.querySelector('#close').focus();
49
+ }
50
+ }
51
+ function closeHelp() {
52
+ ++generation;
53
+ pending?.abort();
54
+ mounted?.unmount();
55
+ mounted = undefined;
56
+ drawer.hidden = true;
57
+ opener?.focus();
58
+ }
59
+ for (const [id, ref] of [
60
+ ['page-help', { kind: 'page', documentId: 'guide.billing' }],
61
+ ['section-help', { kind: 'section', documentId: 'guide.billing', sectionId: 'refunds' }],
62
+ ]) document.getElementById(id).addEventListener('click', event => { opener = event.currentTarget; openHelp(ref); });
63
+ container.addEventListener('click', event => {
64
+ const anchor = event.target.closest('a[data-export-link]');
65
+ const link = anchor && mounted?.links.find(item => item.key === anchor.dataset.exportLink);
66
+ if (link?.kind !== 'resource') return;
67
+ event.preventDefault();
68
+ openHelp(link.ref, link.fragment);
69
+ });
70
+ document.querySelector('#close').addEventListener('click', closeHelp);
71
+ drawer.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); closeHelp(); } });
72
+
73
+ source.then(async content => {
74
+ if (disposed) { content.dispose(); return; }
75
+ snapshot = content;
76
+ const result = await content.get({ kind: 'snippet', id: 'billing.refund-window' }, { format: 'html', signal: lifetime.signal });
77
+ if (disposed) return;
78
+ for (const id of ['plan-note', 'invoice-note']) notes.push(mountContent(document.getElementById(id), result));
79
+ }).catch(error => { if (!disposed) document.querySelector('#status').textContent = `Help could not be loaded: ${error.message}`; });
80
+ window.addEventListener('pagehide', event => {
81
+ // A back/forward-cache entry resumes this same module and snapshot. Preserve
82
+ // its state while the browser freezes it; only tear down a discarded page.
83
+ if (event.persisted) return;
84
+ disposed = true;
85
+ ++generation;
86
+ lifetime.abort();
87
+ pending?.abort();
88
+ mounted?.unmount();
89
+ notes.forEach(note => note.unmount());
90
+ snapshot?.dispose();
91
+ });
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: guide.billing
3
+ title: Billing help
4
+ summary: Find invoices and request a refund for your workspace.
5
+ slug: /billing
6
+ standaloneSections:
7
+ - { id: refunds, context: section }
8
+ ---
9
+
10
+ Use this guide to manage payments for your workspace.
11
+
12
+ ## Find an invoice
13
+
14
+ Open **Settings → Billing → Invoices** and choose the payment date.
15
+
16
+ ## Request a refund <Anchor id="refunds" />
17
+
18
+ <Snippet id="shared.refund-window" />
19
+
20
+ The workspace owner can open **Settings → Billing → Contact support** and provide the invoice number. Support confirms eligibility before issuing a refund.
21
+
22
+ [Read the billing overview](doc:guide.billing).
@@ -0,0 +1,36 @@
1
+ schemaVersion: 1
2
+ site:
3
+ title: Acme billing help
4
+ origin: https://docs.example.com
5
+ publications:
6
+ defaults: { channel: production, version: current, language: en }
7
+ channels:
8
+ production: { pathPrefix: "" }
9
+ versions:
10
+ current: { pathPrefix: "" }
11
+ enabled:
12
+ - { channel: production, version: current }
13
+ frontmatter:
14
+ defaults:
15
+ type: guide
16
+ channel: production
17
+ version: current
18
+ access: { policy: public }
19
+ snippets:
20
+ shared.refund-window:
21
+ file: ./snippets/refund-window.mdx
22
+ variables:
23
+ days: { type: number, default: 30 }
24
+ exports:
25
+ app-help:
26
+ formats: [html, markdown]
27
+ publicBase: /help/releases/demo/
28
+ resources:
29
+ - { kind: page, documentId: guide.billing }
30
+ - { kind: section, documentId: guide.billing, sectionId: refunds }
31
+ - kind: snippet
32
+ id: billing.refund-window
33
+ snippetId: shared.refund-window
34
+ title: Refund window
35
+ summary: When a workspace can request a refund.
36
+ values: { days: 30 }
@@ -0,0 +1,25 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <title>Acme billing · Content delivery example</title>
6
+ <style>
7
+ *{box-sizing:border-box}body{font:16px/1.6 system-ui,sans-serif;color:#193b2c;background:#f4f7f4;margin:0}main{max-width:60rem;margin:3rem auto;padding:0 1.5rem}h1{line-height:1.2}button,a{color:#205b3b}button{font:inherit;cursor:pointer;padding:.5rem .8rem;border:1px solid #aac5b2;border-radius:.4rem;background:white}button:focus-visible,a:focus-visible{outline:3px solid #47926b;outline-offset:3px}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));gap:1rem}.card{padding:1.5rem;border:1px solid #d6e3d9;border-radius:.75rem;background:white}.card h2{margin-top:0}.help-content{font-size:.95rem}.help-content h2{font-size:1.3rem}.help-content a{overflow-wrap:anywhere}#drawer{position:fixed;inset:0 0 0 auto;width:min(36rem,95vw);padding:1.5rem;background:white;overflow:auto;box-shadow:-1rem 0 3rem #193b2c22}#drawer[hidden]{display:none}.drawer-header{display:flex;align-items:center;justify-content:space-between;gap:1rem;border-bottom:1px solid #d6e3d9;padding-bottom:1rem}.muted{color:#526f5d}#status{min-height:1.5em}
8
+ </style>
9
+ <main>
10
+ <p class="muted">Acme workspace</p>
11
+ <h1>Billing</h1>
12
+ <p>Manage your plan and recent payments.</p>
13
+ <p><button id="page-help">Billing guide</button> <button id="section-help">How to request a refund</button></p>
14
+ <div class="cards">
15
+ <section class="card" aria-labelledby="plan-title"><h2 id="plan-title">Current plan</h2><p>Team · $24 per month</p><div id="plan-note" class="help-content" aria-label="Plan refund policy"></div></section>
16
+ <section class="card" aria-labelledby="invoice-title"><h2 id="invoice-title">Recent payment</h2><p>Invoice INV-1042 · $24</p><div id="invoice-note" class="help-content" aria-label="Invoice refund policy"></div></section>
17
+ </div>
18
+ <p id="status" role="status" class="muted"></p>
19
+ </main>
20
+ <aside id="drawer" aria-label="Billing help" hidden>
21
+ <div class="drawer-header"><strong>Billing help</strong><button id="close">Close help</button></div>
22
+ <div id="help-content" class="help-content" aria-live="polite"></div>
23
+ </aside>
24
+ <script type="module" src="/app.js"></script>
25
+ </html>
@@ -0,0 +1,86 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFile, realpath } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import path from 'node:path';
5
+ import { openManifestContentSource } from '@shoalrun/cli/content';
6
+ import { createContentRestHandler } from '@shoalrun/cli/content/server';
7
+
8
+ const root = path.dirname(fileURLToPath(import.meta.url));
9
+ let port = 8775;
10
+ let directory = path.join(root, 'public/help/releases/demo');
11
+ const args = process.argv.slice(2);
12
+ for (let index = 0; index < args.length; index++) {
13
+ if (args[index] === '--help') { console.log('Usage: node preview.mjs [--port PORT] [--help-directory DIRECTORY]'); process.exit(0); }
14
+ if (args[index] === '--port') port = Number(args[++index]);
15
+ else if (args[index] === '--help-directory' && args[index + 1]) directory = path.resolve(args[++index]);
16
+ else throw new Error('Unknown preview option');
17
+ }
18
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Invalid port');
19
+ const help = await realpath(directory);
20
+ const clientEntry = fileURLToPath(import.meta.resolve('@shoalrun/cli/content'));
21
+ const clientDirectory = path.dirname(clientEntry);
22
+ // Serve only the public browser entry and its browser-safe dependencies.
23
+ // This is deliberately not a generic node_modules or repository file server.
24
+ const clientFiles = new Map([
25
+ ['/shoalrun/content-delivery.js', clientEntry],
26
+ ['/shoalrun/content-delivery-contract.js', path.join(clientDirectory, 'content-delivery-contract.js')],
27
+ ...['content-mount', 'content-delivery-validation', 'content-delivery-transport', 'content-rest-client', 'content-rest-contract'].map(name => [`/shoalrun/${name}.js`, path.join(clientDirectory, `${name}.js`)]),
28
+ ]);
29
+ const mount = '/help/releases/demo/';
30
+ const types = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.md': 'text/markdown; charset=utf-8', '.png': 'image/png' };
31
+ // Pin one approved bundle at startup. The injected transport reads only this
32
+ // bundle; realpath containment applies even to symlinks. It does not fetch URLs.
33
+ const source = await openManifestContentSource({
34
+ manifestUrl: `http://content.local${mount}manifest.json`,
35
+ fetch: async (input, options) => {
36
+ options?.signal?.throwIfAborted();
37
+ const url = new URL(String(input));
38
+ if (url.origin !== 'http://content.local' || !url.pathname.startsWith(mount)) throw new Error('Outside bundle');
39
+ try {
40
+ const file = await realpath(path.join(help, url.pathname.slice(mount.length)));
41
+ if (!file.startsWith(help + path.sep)) throw new Error('Outside bundle');
42
+ const body = await readFile(file, { signal: options?.signal ?? undefined });
43
+ return new Response(body, { headers: { 'Content-Type': types[path.extname(file)] ?? 'application/octet-stream' } });
44
+ } catch (error) {
45
+ options?.signal?.throwIfAborted();
46
+ return new Response(null, { status: 404 });
47
+ }
48
+ },
49
+ });
50
+ const rest = createContentRestHandler({ sources: [source] });
51
+ const server = createServer(async (request, response) => {
52
+ const url = new URL(request.url, 'http://127.0.0.1');
53
+ if (url.pathname === '/content/v1' || url.pathname.startsWith('/content/v1/')) {
54
+ const controller = new AbortController();
55
+ request.once('aborted', () => controller.abort());
56
+ response.once('close', () => { if (!response.writableEnded) controller.abort(); });
57
+ try {
58
+ const headers = new Headers();
59
+ for (const [name, value] of Object.entries(request.headers)) if (value !== undefined) headers.set(name, Array.isArray(value) ? value.join(', ') : value);
60
+ const result = await rest(new Request(url, { method: request.method, headers, signal: controller.signal }));
61
+ response.writeHead(result.status, Object.fromEntries(result.headers));
62
+ response.end(Buffer.from(await result.arrayBuffer()));
63
+ } catch {
64
+ if (!response.destroyed) { response.writeHead(500, { 'Cache-Control': 'no-store' }); response.end(); }
65
+ }
66
+ return;
67
+ }
68
+ if (!['GET', 'HEAD'].includes(request.method)) { response.writeHead(405, { Allow: 'GET, HEAD' }); response.end(); return; }
69
+ try {
70
+ const pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname);
71
+ let file = clientFiles.get(pathname);
72
+ if (!file && pathname.startsWith(mount)) {
73
+ file = await realpath(path.join(help, pathname.slice(mount.length)));
74
+ if (!file.startsWith(help + path.sep)) throw new Error('Invalid path');
75
+ }
76
+ if (!file && pathname === '/app.js') file = path.join(root, 'app.js');
77
+ if (!file && (pathname === '/' || pathname === '/settings/billing')) file = path.join(root, 'index.html');
78
+ if (!file) throw new Error('Not found');
79
+ const body = await readFile(file);
80
+ response.writeHead(200, { 'Content-Type': types[path.extname(file)] ?? 'application/octet-stream', 'X-Content-Type-Options': 'nosniff' });
81
+ response.end(request.method === 'HEAD' ? undefined : body);
82
+ } catch { response.writeHead(404); response.end('Not found'); }
83
+ });
84
+ server.listen(port, '127.0.0.1', () => console.log(`Content delivery demo: http://127.0.0.1:${server.address().port}/settings/billing`));
85
+ process.on('SIGINT', () => server.close(() => { source.dispose(); process.exit(0); }));
86
+ process.on('SIGTERM', () => server.close(() => { source.dispose(); process.exit(0); }));
@@ -0,0 +1,3 @@
1
+ You can request a refund within <Var name="days" /> days of payment.[^terms]
2
+
3
+ [^terms]: Refund eligibility depends on the workspace agreement.
@@ -18,10 +18,10 @@ billing help buttons; follow the in-drawer links and inspect the images.
18
18
  The full account guide is omitted from the export and links to the configured
19
19
  example documentation host; replace that origin for your own project.
20
20
 
21
- `manifest.json` maps stable document IDs to representation files. `app.js`
21
+ `manifest.json` maps typed page references to representation files in its `resources` array. `app.js`
22
22
  fetches the HTML, inserts it into one content container, and intercepts anchors
23
23
  marked `data-export-link`. The corresponding manifest link supplies the target
24
- document ID and original fragment. Other links use their ordinary destinations.
24
+ page reference and original fragment. Other links use their ordinary destinations.
25
25
 
26
26
  `/help/` is an example path, not a required Shoalrun directory. You can choose
27
27
  another root-relative path, such as `/resources/`. Set `publicBase` to that path,
@@ -14,7 +14,7 @@ let current;
14
14
  let request = 0;
15
15
  let opener;
16
16
  async function openHelp(id, fragment) {
17
- const document = manifest.documents.find(document => document.id === id);
17
+ const document = manifest.resources.find(resource => resource.ref.kind === 'page' && resource.ref.documentId === id);
18
18
  if (!document?.representations.html) throw new Error(`No HTML help for ${id}`);
19
19
  const ticket = ++request;
20
20
  const response = await fetch(manifest.publicBase + document.representations.html.path);
@@ -43,9 +43,9 @@ content.addEventListener('click', event => {
43
43
  const anchor = event.target.closest('a[data-export-link]');
44
44
  if (!anchor) return;
45
45
  const link = current?.representations.html.links.find(link => link.key === anchor.dataset.exportLink);
46
- if (link?.kind !== 'document') return; // Canonical fallbacks open as ordinary links.
46
+ if (link?.kind !== 'resource' || link.ref?.kind !== 'page') return; // Canonical fallbacks open as ordinary links.
47
47
  event.preventDefault();
48
- openHelp(link.documentId, link.fragment).catch(showError);
48
+ openHelp(link.ref.documentId, link.fragment).catch(showError);
49
49
  });
50
50
  function closeHelp() { request++; drawer.hidden = true; opener?.focus(); }
51
51
  document.querySelector('#close').addEventListener('click', closeHelp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoalrun/cli",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.14",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public"
@@ -11,9 +11,11 @@
11
11
  "license": "Apache-2.0",
12
12
  "type": "module",
13
13
  "exports": {
14
+ "./content": "./dist/content-delivery.js",
14
15
  "./authoring": "./dist/authoring-api.js",
15
16
  "./*": "./*",
16
- "./components": "./dist/components.js"
17
+ "./components": "./dist/components.js",
18
+ "./content/server": "./dist/content-rest-server.js"
17
19
  },
18
20
  "files": [
19
21
  "dist",
@@ -23,7 +25,7 @@
23
25
  "styles",
24
26
  "templates",
25
27
  "examples",
26
- "!examples/help-drawer/public/**",
28
+ "!examples/**/public/**",
27
29
  "!examples/**/node_modules/**",
28
30
  "example-configs",
29
31
  "!example-configs/**/.shoalrun/**",
@@ -32,6 +34,7 @@
32
34
  "README.md",
33
35
  "AUTHORING.md",
34
36
  "schemas/content-export.schema.json",
37
+ "schemas/content-rest.openapi.json",
35
38
  "CHANGELOG.md",
36
39
  "!**/*.local.md",
37
40
  "!**/*.override.md",