@o-a/cms-agent 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 (201) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +132 -0
  3. package/dist/boot.d.ts +15 -0
  4. package/dist/boot.js +33 -0
  5. package/dist/config.d.ts +19 -0
  6. package/dist/config.js +45 -0
  7. package/dist/create-site/cli.d.ts +2 -0
  8. package/dist/create-site/cli.js +31 -0
  9. package/dist/create-site/generate-site.d.ts +9 -0
  10. package/dist/create-site/generate-site.js +109 -0
  11. package/dist/create-site/mint-token-cli.d.ts +2 -0
  12. package/dist/create-site/mint-token-cli.js +26 -0
  13. package/dist/create-site/mint-token.d.ts +7 -0
  14. package/dist/create-site/mint-token.js +49 -0
  15. package/dist/create-site/template/content/menus/footerCompany.json +9 -0
  16. package/dist/create-site/template/content/menus/footerProduct.json +9 -0
  17. package/dist/create-site/template/content/menus/footerResources.json +9 -0
  18. package/dist/create-site/template/content/menus/main.json +9 -0
  19. package/dist/create-site/template/content/pages/404.json +22 -0
  20. package/dist/create-site/template/content/pages/about/careers.json +21 -0
  21. package/dist/create-site/template/content/pages/about/team.json +21 -0
  22. package/dist/create-site/template/content/pages/about.json +21 -0
  23. package/dist/create-site/template/content/pages/docs/deployment.json +21 -0
  24. package/dist/create-site/template/content/pages/docs/getting-started/quickstart.json +21 -0
  25. package/dist/create-site/template/content/pages/docs/getting-started.json +23 -0
  26. package/dist/create-site/template/content/pages/docs.json +21 -0
  27. package/dist/create-site/template/content/pages/index.json +181 -0
  28. package/dist/create-site/template/content/redirects.json +4 -0
  29. package/dist/create-site/template/dockerignore +6 -0
  30. package/dist/create-site/template/gitignore +11 -0
  31. package/dist/create-site/template/theme/assets/site.js +49 -0
  32. package/dist/create-site/template/theme/assets/style.css +1153 -0
  33. package/dist/create-site/template/theme/blocks/button.liquid +21 -0
  34. package/dist/create-site/template/theme/blocks/comparison-row.liquid +19 -0
  35. package/dist/create-site/template/theme/blocks/faq-item.liquid +22 -0
  36. package/dist/create-site/template/theme/blocks/feature-card.liquid +23 -0
  37. package/dist/create-site/template/theme/blocks/logo-mark.liquid +13 -0
  38. package/dist/create-site/template/theme/blocks/pricing-tier.liquid +42 -0
  39. package/dist/create-site/template/theme/blocks/step-card.liquid +19 -0
  40. package/dist/create-site/template/theme/blocks/testimonial-card.liquid +25 -0
  41. package/dist/create-site/template/theme/layouts/theme.liquid +99 -0
  42. package/dist/create-site/template/theme/sections/comparison.liquid +41 -0
  43. package/dist/create-site/template/theme/sections/cta-banner.liquid +27 -0
  44. package/dist/create-site/template/theme/sections/faq.liquid +27 -0
  45. package/dist/create-site/template/theme/sections/feature-grid.liquid +27 -0
  46. package/dist/create-site/template/theme/sections/hero.liquid +36 -0
  47. package/dist/create-site/template/theme/sections/how-it-works.liquid +27 -0
  48. package/dist/create-site/template/theme/sections/pricing.liquid +27 -0
  49. package/dist/create-site/template/theme/sections/social-proof.liquid +21 -0
  50. package/dist/create-site/template/theme/sections/testimonials.liquid +27 -0
  51. package/dist/create-site/template/theme/snippets/icon.liquid +24 -0
  52. package/dist/create-site/template/theme/snippets/logo.liquid +7 -0
  53. package/dist/create-site/template/theme/snippets/site-name.liquid +1 -0
  54. package/dist/create-site/template/vhost/Dockerfile +33 -0
  55. package/dist/create-site/template/vhost/docker-entrypoint.sh +30 -0
  56. package/dist/index.d.ts +2 -0
  57. package/dist/index.js +4 -0
  58. package/dist/media/drivers/driver.d.ts +11 -0
  59. package/dist/media/drivers/driver.js +1 -0
  60. package/dist/media/drivers/local-fs-driver.d.ts +3 -0
  61. package/dist/media/drivers/local-fs-driver.js +81 -0
  62. package/dist/media/filename.d.ts +1 -0
  63. package/dist/media/filename.js +31 -0
  64. package/dist/media/manage-media.d.ts +19 -0
  65. package/dist/media/manage-media.js +45 -0
  66. package/dist/migrations/index.d.ts +3 -0
  67. package/dist/migrations/index.js +49 -0
  68. package/dist/renderer/engine.d.ts +2 -0
  69. package/dist/renderer/engine.js +48 -0
  70. package/dist/renderer/render-page.d.ts +28 -0
  71. package/dist/renderer/render-page.js +143 -0
  72. package/dist/renderer/theme-templates.d.ts +7 -0
  73. package/dist/renderer/theme-templates.js +82 -0
  74. package/dist/routes/assets.d.ts +6 -0
  75. package/dist/routes/assets.js +30 -0
  76. package/dist/routes/batch.d.ts +10 -0
  77. package/dist/routes/batch.js +148 -0
  78. package/dist/routes/capabilities.d.ts +5 -0
  79. package/dist/routes/capabilities.js +27 -0
  80. package/dist/routes/content.d.ts +8 -0
  81. package/dist/routes/content.js +166 -0
  82. package/dist/routes/drafts.d.ts +10 -0
  83. package/dist/routes/drafts.js +73 -0
  84. package/dist/routes/git.d.ts +8 -0
  85. package/dist/routes/git.js +158 -0
  86. package/dist/routes/index.d.ts +19 -0
  87. package/dist/routes/index.js +84 -0
  88. package/dist/routes/media-public.d.ts +5 -0
  89. package/dist/routes/media-public.js +48 -0
  90. package/dist/routes/media.d.ts +9 -0
  91. package/dist/routes/media.js +84 -0
  92. package/dist/routes/menus.d.ts +8 -0
  93. package/dist/routes/menus.js +77 -0
  94. package/dist/routes/preview-revision.d.ts +13 -0
  95. package/dist/routes/preview-revision.js +109 -0
  96. package/dist/routes/preview.d.ts +13 -0
  97. package/dist/routes/preview.js +52 -0
  98. package/dist/routes/public.d.ts +11 -0
  99. package/dist/routes/public.js +117 -0
  100. package/dist/routes/publish.d.ts +10 -0
  101. package/dist/routes/publish.js +105 -0
  102. package/dist/routes/redirects.d.ts +8 -0
  103. package/dist/routes/redirects.js +132 -0
  104. package/dist/routes/search.d.ts +8 -0
  105. package/dist/routes/search.js +11 -0
  106. package/dist/routes/sitemap.d.ts +6 -0
  107. package/dist/routes/sitemap.js +79 -0
  108. package/dist/routes/theme.d.ts +10 -0
  109. package/dist/routes/theme.js +17 -0
  110. package/dist/schemas/instance.schema.json +17 -0
  111. package/dist/schemas/menu.schema.json +23 -0
  112. package/dist/schemas/page.schema.json +20 -0
  113. package/dist/schemas/post.schema.json +25 -0
  114. package/dist/schemas/redirects.schema.json +24 -0
  115. package/dist/search/drivers/driver.d.ts +9 -0
  116. package/dist/search/drivers/driver.js +1 -0
  117. package/dist/search/drivers/node-sqlite-driver.d.ts +3 -0
  118. package/dist/search/drivers/node-sqlite-driver.js +19 -0
  119. package/dist/search/query-index.d.ts +5 -0
  120. package/dist/search/query-index.js +21 -0
  121. package/dist/search/rebuild-index.d.ts +2 -0
  122. package/dist/search/rebuild-index.js +111 -0
  123. package/dist/server-config.d.ts +22 -0
  124. package/dist/server-config.js +196 -0
  125. package/dist/server.d.ts +13 -0
  126. package/dist/server.js +163 -0
  127. package/dist/services/batch.d.ts +35 -0
  128. package/dist/services/batch.js +136 -0
  129. package/dist/services/checkpoint.d.ts +4 -0
  130. package/dist/services/checkpoint.js +45 -0
  131. package/dist/services/content-read.d.ts +27 -0
  132. package/dist/services/content-read.js +156 -0
  133. package/dist/services/delete-content.d.ts +12 -0
  134. package/dist/services/delete-content.js +174 -0
  135. package/dist/services/dev-tunnel.d.ts +5 -0
  136. package/dist/services/dev-tunnel.js +5 -0
  137. package/dist/services/drafts.d.ts +15 -0
  138. package/dist/services/drafts.js +155 -0
  139. package/dist/services/etag.d.ts +2 -0
  140. package/dist/services/etag.js +30 -0
  141. package/dist/services/fs-walk.d.ts +1 -0
  142. package/dist/services/fs-walk.js +26 -0
  143. package/dist/services/git-commit.d.ts +3 -0
  144. package/dist/services/git-commit.js +12 -0
  145. package/dist/services/git-history.d.ts +28 -0
  146. package/dist/services/git-history.js +131 -0
  147. package/dist/services/git-revert.d.ts +12 -0
  148. package/dist/services/git-revert.js +140 -0
  149. package/dist/services/git.d.ts +16 -0
  150. package/dist/services/git.js +127 -0
  151. package/dist/services/interval-job.d.ts +4 -0
  152. package/dist/services/interval-job.js +9 -0
  153. package/dist/services/ip-allowlist.d.ts +7 -0
  154. package/dist/services/ip-allowlist.js +31 -0
  155. package/dist/services/manage-menus.d.ts +10 -0
  156. package/dist/services/manage-menus.js +84 -0
  157. package/dist/services/manage-redirects.d.ts +25 -0
  158. package/dist/services/manage-redirects.js +166 -0
  159. package/dist/services/menus.d.ts +8 -0
  160. package/dist/services/menus.js +42 -0
  161. package/dist/services/migration-runner.d.ts +13 -0
  162. package/dist/services/migration-runner.js +123 -0
  163. package/dist/services/mime-types.d.ts +3 -0
  164. package/dist/services/mime-types.js +36 -0
  165. package/dist/services/move.d.ts +16 -0
  166. package/dist/services/move.js +175 -0
  167. package/dist/services/path-safety.d.ts +6 -0
  168. package/dist/services/path-safety.js +112 -0
  169. package/dist/services/post-urls.d.ts +3 -0
  170. package/dist/services/post-urls.js +27 -0
  171. package/dist/services/prepared-operation.d.ts +4 -0
  172. package/dist/services/prepared-operation.js +1 -0
  173. package/dist/services/publish.d.ts +14 -0
  174. package/dist/services/publish.js +254 -0
  175. package/dist/services/rate-limit-config.d.ts +9 -0
  176. package/dist/services/rate-limit-config.js +20 -0
  177. package/dist/services/redirects.d.ts +27 -0
  178. package/dist/services/redirects.js +175 -0
  179. package/dist/services/resolve-blog-url.d.ts +11 -0
  180. package/dist/services/resolve-blog-url.js +31 -0
  181. package/dist/services/resolve-url.d.ts +11 -0
  182. package/dist/services/resolve-url.js +20 -0
  183. package/dist/services/startup-checks.d.ts +10 -0
  184. package/dist/services/startup-checks.js +81 -0
  185. package/dist/services/static-file.d.ts +7 -0
  186. package/dist/services/static-file.js +37 -0
  187. package/dist/services/theme-component-file.d.ts +5 -0
  188. package/dist/services/theme-component-file.js +32 -0
  189. package/dist/services/theme-page-templates.d.ts +7 -0
  190. package/dist/services/theme-page-templates.js +53 -0
  191. package/dist/services/theme-schemas.d.ts +2 -0
  192. package/dist/services/theme-schemas.js +63 -0
  193. package/dist/services/token-auth.d.ts +10 -0
  194. package/dist/services/token-auth.js +64 -0
  195. package/dist/services/urls.d.ts +2 -0
  196. package/dist/services/urls.js +20 -0
  197. package/dist/services/validation.d.ts +24 -0
  198. package/dist/services/validation.js +158 -0
  199. package/dist/services/write-queue.d.ts +1 -0
  200. package/dist/services/write-queue.js +14 -0
  201. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Scott Gray
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # Granite CMS
2
+
3
+ **Own your content, forever.**
4
+
5
+ Granite CMS is a self-hosted, git-backed content management system. Every
6
+ page is a plain JSON file. Every publish is a real git commit. There is no
7
+ database standing between you and your own content, and there never will be.
8
+
9
+ If you've ever had a headless CMS raise its prices, change its export
10
+ format, or go down and take your site's editing with it - this is built to
11
+ make that structurally impossible. Moving away from Granite CMS is
12
+ `git clone`. That's the whole migration story.
13
+
14
+ ## Why this is different
15
+
16
+ Most self-hosted, code-first CMS options give you an API and a database.
17
+ Most git-based CMS tools give you markdown files and a static-site rebuild.
18
+ Granite CMS is the combination neither of those is:
19
+
20
+ - **Git-native content.** Pages, posts, menus, and redirects are JSON files
21
+ in your own git repository. No database is ever the source of truth.
22
+ History, diffs, and rollback are real git operations, not a bolted-on
23
+ revisions table.
24
+ - **A real page builder, not just files.** Themes define reusable, schema
25
+ validated **sections** and **blocks** - a Shopify-style content model - so
26
+ a non-technical editor arranges and fills in components a developer
27
+ already built, instead of hand-editing markdown or JSON.
28
+ - **A real versioned HTTP API.** `/v1/...` is the only thing a site exposes.
29
+ Drafts, publishing, optimistic concurrency (ETags + `If-Match`), batch
30
+ writes, and git history are all first-class API operations - what a
31
+ separate admin application (or your own tooling) talks to, never the
32
+ filesystem directly.
33
+
34
+ ## Two audiences, one engine
35
+
36
+ - **Developers** build the theme: Liquid layouts, sections, and blocks,
37
+ each with an embedded JSON Schema for its settings. Start with
38
+ [`docs/theme-authoring-guide.md`](docs/theme-authoring-guide.md).
39
+ - **Content editors / marketing managers** never touch this repository at
40
+ all - they work entirely through a separate admin application (see
41
+ [Companion projects](#companion-projects) below), browsing pages,
42
+ editing sections, and publishing through the API this package exposes.
43
+ The content model itself is documented in
44
+ [`docs/content-authoring-guide.md`](docs/content-authoring-guide.md).
45
+
46
+ ## Quick start
47
+
48
+ > **Not yet published to npm.** Until it is, install from a local build -
49
+ > see [`docs/hosting.md`](docs/hosting.md#installing-o-acms-agent-today) for
50
+ > the exact steps. Once published, this becomes:
51
+ >
52
+ > ```
53
+ > npx -p @o-a/cms-agent create-site my-site
54
+ > ```
55
+
56
+ Either way, the result is the same:
57
+
58
+ ```
59
+ cd my-site/vhost
60
+ npm install
61
+ node server.js
62
+ ```
63
+
64
+ Visit `http://localhost:3000`. `create-site` prints a real API token to
65
+ your terminal when it scaffolds the site - that's what a separate admin
66
+ application (or a plain `curl`) uses to authenticate against the site's
67
+ `/v1/` API. Lost it, or need a second one? See the `mint-token` command in
68
+ the same docs page.
69
+
70
+ ## Project layout
71
+
72
+ A scaffolded site is a thin, four-folder scaffold - everything a content
73
+ editor touches lives under `content/`, everything a developer touches lives
74
+ under `theme/`, uploaded media lives under `media/` (never git-tracked),
75
+ and the site's own serving configuration lives under `vhost/`:
76
+
77
+ ```
78
+ my-site/
79
+ content/ pages, posts, menus, redirects, drafts
80
+ theme/ layouts, sections, blocks, snippets, assets, root, templates
81
+ media/ uploaded files - gitignored, backed up separately
82
+ vhost/ site.config.json, package.json, server.js
83
+ ```
84
+
85
+ This repository (the agent itself) is developed separately and installed
86
+ as a versioned dependency - see [`docs/cms-build-plan.md`](docs/cms-build-plan.md)
87
+ for the full architecture and the reasoning behind it.
88
+
89
+ ## Tech stack
90
+
91
+ Node.js 22+ and TypeScript, compiled to plain JavaScript for distribution.
92
+ Fastify, LiquidJS (sandboxed - no dynamically registered tags or filters,
93
+ ever), SQLite via `node:sqlite` for a fully disposable, rebuildable search
94
+ index, and the real `git` binary for every content mutation. Dependencies
95
+ are kept deliberately minimal - this ships as a package other people
96
+ install.
97
+
98
+ ## Documentation
99
+
100
+ - [`docs/theme-authoring-guide.md`](docs/theme-authoring-guide.md) - building a theme
101
+ - [`docs/content-authoring-guide.md`](docs/content-authoring-guide.md) - the content model
102
+ - [`docs/hosting.md`](docs/hosting.md) - running a site somewhere real
103
+ - [`docs/cms-build-plan.md`](docs/cms-build-plan.md) - full architecture and design rationale
104
+
105
+ A friendlier, browsable documentation site (covering both this engine and
106
+ the admin application) is also available in source form at
107
+ [o-a-com-au/o-a-com-au-granitecms-docs](https://github.com/o-a-com-au/o-a-com-au-granitecms-docs).
108
+
109
+ ## Companion projects
110
+
111
+ Granite CMS the engine has no user interface of its own by design - it's an
112
+ API. Day-to-day editing happens through a separate admin application
113
+ (`granitecms-admin`), a control plane that can manage any number of
114
+ independently hosted sites. The two are deliberately separate codebases
115
+ with separate deploy cycles, connected only by the versioned `/v1/` API
116
+ above.
117
+
118
+ ## Project status
119
+
120
+ This is young, real software - it runs a genuine production site today,
121
+ not just a demo, but it has one tenant so far, not thousands. Expect rough
122
+ edges, expect things to improve in the open rather than arrive finished,
123
+ and please open an issue if something doesn't work the way this README
124
+ says it should.
125
+
126
+ ## License
127
+
128
+ [Functional Source License, Version 1.1, Apache 2.0 Future Grant](LICENSE)
129
+ (FSL-1.1-ALv2). In short: free to use, modify, and self-host for anything
130
+ except offering it as a competing hosted service. Every version converts
131
+ automatically to the fully permissive Apache License 2.0 two years after
132
+ its own release - this is a head start, not a permanent restriction.
package/dist/boot.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Liquid } from 'liquidjs';
2
+ import type { SiteConfig } from './config.ts';
3
+ import type { ThemeTemplates } from './renderer/theme-templates.ts';
4
+ import type { StartupCheckOptions } from './services/startup-checks.ts';
5
+ import type { ThemeSchemas } from './services/validation.ts';
6
+ import type { PageTemplate } from './services/theme-page-templates.ts';
7
+ export interface BootedSite {
8
+ config: SiteConfig;
9
+ themeSchemas: ThemeSchemas;
10
+ themeTemplates: ThemeTemplates;
11
+ layouts: Record<string, string>;
12
+ pageTemplates: PageTemplate[];
13
+ engine: Liquid;
14
+ }
15
+ export declare function bootSite(siteRoot: string, options?: StartupCheckOptions): BootedSite;
package/dist/boot.js ADDED
@@ -0,0 +1,33 @@
1
+ import { loadSiteConfig } from "./config.js";
2
+ import { createEngine } from "./renderer/engine.js";
3
+ import { loadLayouts, loadSnippets, loadThemeTemplates } from "./renderer/theme-templates.js";
4
+ import { runStartupChecks } from "./services/startup-checks.js";
5
+ import { loadThemeSchemas } from "./services/theme-schemas.js";
6
+ import { loadPageTemplates } from "./services/theme-page-templates.js";
7
+ // The composition root: startup checks, config, and theme loading, in
8
+ // that order, taking nothing but a site root path (checklist H1: "the
9
+ // agent boots against it... with no steps beyond config"). This is
10
+ // what a future Phase 2 server.js will call before starting the
11
+ // Fastify listener.
12
+ //
13
+ // Deliberately does NOT run migrations here: migration is a
14
+ // git-committing operation requiring an author identity, and what the
15
+ // real default identity should be when this runs unattended at boot
16
+ // is a genuine open question already deferred to Phase 2 wiring (see
17
+ // migration-runner.ts's commit message) - silently inventing one here
18
+ // to wire it into boot would quietly resolve that same open question
19
+ // a second time, inconsistently.
20
+ export function bootSite(siteRoot, options) {
21
+ runStartupChecks(siteRoot, options);
22
+ const config = loadSiteConfig(siteRoot);
23
+ const themeSchemas = loadThemeSchemas(config.themeRoot);
24
+ const themeTemplates = loadThemeTemplates(config.themeRoot);
25
+ const layouts = loadLayouts(config.themeRoot);
26
+ const snippets = loadSnippets(config.themeRoot);
27
+ // After themeSchemas - a template's own sections/blocks are validated
28
+ // against the theme's real section/block schemas, same as any real
29
+ // page.
30
+ const pageTemplates = loadPageTemplates(config.templatesRoot, themeSchemas);
31
+ const engine = createEngine(snippets);
32
+ return { config, themeSchemas, themeTemplates, layouts, pageTemplates, engine };
33
+ }
@@ -0,0 +1,19 @@
1
+ export declare const VHOST_DIR_NAME = "vhost";
2
+ export interface SiteConfig {
3
+ siteRoot: string;
4
+ contentRoot: string;
5
+ draftsRoot: string;
6
+ themeRoot: string;
7
+ assetsRoot: string;
8
+ rootMirrorRoot: string;
9
+ templatesRoot: string;
10
+ pagesRoot: string;
11
+ postsRoot: string;
12
+ menusRoot: string;
13
+ redirectsPath: string;
14
+ vhostRoot: string;
15
+ dataRoot: string;
16
+ searchIndexPath: string;
17
+ mediaRoot: string;
18
+ }
19
+ export declare function loadSiteConfig(siteRoot: string): SiteConfig;
package/dist/config.js ADDED
@@ -0,0 +1,45 @@
1
+ import { isAbsolute, join, resolve } from 'node:path';
2
+ // Single source of truth for the runtime/serving folder's name, reused
3
+ // by server-config.ts (site.config.json lives inside it) rather than a
4
+ // second hardcoded literal. A real vhost, in hosting terms (Apache/
5
+ // nginx/CyberPanel): the per-site serving configuration, not a
6
+ // multi-tenancy concept - this folder holds exactly that (package.json,
7
+ // server.js, site.config.json's access-control/serving parameters).
8
+ export const VHOST_DIR_NAME = 'vhost';
9
+ // Pure path derivation, no fs calls. siteRoot must be supplied by the
10
+ // caller (agent configuration) and must already be absolute: the agent
11
+ // never assumes it lives inside the site repo, and never falls back to
12
+ // resolving against its own module location or process.cwd().
13
+ export function loadSiteConfig(siteRoot) {
14
+ if (!isAbsolute(siteRoot)) {
15
+ throw new Error(`siteRoot must be an absolute path, got "${siteRoot}"`);
16
+ }
17
+ const normalisedRoot = resolve(siteRoot);
18
+ const contentRoot = join(normalisedRoot, 'content');
19
+ const themeRoot = join(normalisedRoot, 'theme');
20
+ const vhostRoot = join(normalisedRoot, VHOST_DIR_NAME);
21
+ const dataRoot = join(vhostRoot, 'data');
22
+ return {
23
+ siteRoot: normalisedRoot,
24
+ contentRoot,
25
+ // Nested inside contentRoot (content/drafts), not a sibling of it -
26
+ // marketing-manager-managed content lives entirely under content/.
27
+ // See content-read.ts/migration-runner.ts for the inclusion-based
28
+ // walk fix this requires, and checkpoint.ts for the derived (not
29
+ // hardcoded) pathspec fix.
30
+ draftsRoot: join(contentRoot, 'drafts'),
31
+ themeRoot,
32
+ assetsRoot: join(themeRoot, 'assets'),
33
+ rootMirrorRoot: join(themeRoot, 'root'),
34
+ templatesRoot: join(themeRoot, 'templates'),
35
+ pagesRoot: join(contentRoot, 'pages'),
36
+ postsRoot: join(contentRoot, 'posts'),
37
+ menusRoot: join(contentRoot, 'menus'),
38
+ // Also nested inside contentRoot, for the same reason.
39
+ redirectsPath: join(contentRoot, 'redirects.json'),
40
+ vhostRoot,
41
+ dataRoot,
42
+ searchIndexPath: join(dataRoot, 'search-index.sqlite'),
43
+ mediaRoot: join(normalisedRoot, 'media'),
44
+ };
45
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import { ScaffoldError, scaffoldSite } from "./generate-site.js";
4
+ const targetArg = process.argv[2];
5
+ if (!targetArg) {
6
+ console.error('Usage: create-site <directory>');
7
+ process.exit(1);
8
+ }
9
+ try {
10
+ const targetDir = resolve(process.cwd(), targetArg);
11
+ const { raw } = scaffoldSite(targetDir);
12
+ console.log(`Created a new site at ${targetDir}`);
13
+ console.log('');
14
+ console.log('Your API token (save this now - it will not be shown again):');
15
+ console.log(` ${raw}`);
16
+ console.log('');
17
+ console.log('Next steps:');
18
+ console.log(` cd ${targetArg}/vhost`);
19
+ console.log(' npm install');
20
+ console.log(' node server.js');
21
+ console.log('');
22
+ console.log('Want to edit this site from a hosted admin while developing locally?');
23
+ console.log(' node server.js --tunnel');
24
+ }
25
+ catch (error) {
26
+ if (error instanceof ScaffoldError) {
27
+ console.error(error.message);
28
+ process.exit(1);
29
+ }
30
+ throw error;
31
+ }
@@ -0,0 +1,9 @@
1
+ export declare function generateToken(): {
2
+ raw: string;
3
+ hash: string;
4
+ };
5
+ export declare class ScaffoldError extends Error {
6
+ }
7
+ export declare function scaffoldSite(targetDir: string): {
8
+ raw: string;
9
+ };
@@ -0,0 +1,109 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { basename, join } from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { CHECKPOINT_AUTHOR } from "../services/checkpoint.js";
6
+ import { commitPaths } from "../services/git.js";
7
+ // This file's own bundled template (dist/create-site/template, copied
8
+ // there by `npm run build`'s cp step) - the agent's own asset, not
9
+ // site data, matching validation.ts's/capabilities.ts's existing
10
+ // import.meta.dirname-relative pattern for the agent's own bundled
11
+ // files (see test/static/static-analysis.test.ts's B1 allowlist).
12
+ const TEMPLATE_ROOT = join(import.meta.dirname, 'template');
13
+ // The agent's own bundled package.json, reused for the exact version
14
+ // to pin the scaffold's dependency to - never a second hardcoded
15
+ // literal, same reasoning as capabilities.ts's readAgentVersion.
16
+ function readAgentVersion() {
17
+ const packageJsonPath = join(import.meta.dirname, '..', '..', 'package.json');
18
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
19
+ return packageJson.version ?? '0.0.0';
20
+ }
21
+ function sanitisePackageName(dirName) {
22
+ const cleaned = dirName.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
23
+ return cleaned.length > 0 ? cleaned : 'my-site';
24
+ }
25
+ export function generateToken() {
26
+ const raw = randomBytes(32).toString('hex');
27
+ const hash = createHash('sha256').update(raw).digest('hex');
28
+ return { raw, hash };
29
+ }
30
+ // server.js lives in vhost/, one level below the real site root - it
31
+ // states its own fixed, known relationship to its sibling directory
32
+ // rather than the agent inferring anything (constraint 2 stays intact:
33
+ // the site root is always explicit, never assumed from the agent's own
34
+ // location).
35
+ const SERVER_JS = `import { startServer } from '@o-a/cms-agent';
36
+ import { join } from 'node:path';
37
+
38
+ await startServer(join(import.meta.dirname, '..'), { tunnel: process.argv.includes('--tunnel') });
39
+ `;
40
+ export class ScaffoldError extends Error {
41
+ }
42
+ // A standalone, one-shot CLI operation - no running server exists yet
43
+ // for enqueue()'s serialisation to matter, and this whole function
44
+ // runs synchronously start to finish, so there's no concurrent-write
45
+ // hazard to guard against.
46
+ export function scaffoldSite(targetDir) {
47
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
48
+ throw new ScaffoldError(`"${targetDir}" already exists and is not empty`);
49
+ }
50
+ mkdirSync(targetDir, { recursive: true });
51
+ // The static theme/content template, copied verbatim except its own
52
+ // "gitignore"/"dockerignore" (no leading dot - a literal .gitignore
53
+ // or .dockerignore inside the template tree would be silently
54
+ // dropped by npm pack's own packlist logic, verified empirically
55
+ // before this was written).
56
+ cpSync(TEMPLATE_ROOT, targetDir, {
57
+ recursive: true,
58
+ filter: (source) => basename(source) !== 'gitignore' && basename(source) !== 'dockerignore',
59
+ });
60
+ cpSync(join(TEMPLATE_ROOT, 'gitignore'), join(targetDir, '.gitignore'));
61
+ cpSync(join(TEMPLATE_ROOT, 'dockerignore'), join(targetDir, '.dockerignore'));
62
+ // content/drafts/ starts empty - not part of the static template
63
+ // (nothing to copy), created directly so a fresh site has somewhere
64
+ // to write its first draft immediately. Nested inside content/, not
65
+ // a sibling of it - see config.ts's own comment for why.
66
+ mkdirSync(join(targetDir, 'content', 'drafts'), { recursive: true });
67
+ // media/ also starts empty, also created directly (no template
68
+ // content to copy - see config.ts's own comment for why this is a
69
+ // sibling of content/theme/vhost, not nested under content/). Not
70
+ // passed to commitPaths below: media is never git-tracked
71
+ // (constraint 2), even at scaffold time - an empty directory has
72
+ // nothing to commit anyway (git doesn't track empty directories), so
73
+ // this omission is a non-issue mechanically, but it's deliberate,
74
+ // not an oversight, should anyone go looking for why it's absent
75
+ // from that list.
76
+ mkdirSync(join(targetDir, 'media'), { recursive: true });
77
+ // vhost/: the site's own serving configuration (package.json,
78
+ // server.js, site.config.json), matching a real vhost's own scope in
79
+ // hosting terms - never a sibling of content/theme.
80
+ const vhostDir = join(targetDir, 'vhost');
81
+ mkdirSync(vhostDir, { recursive: true });
82
+ // A zero-token site fails closed on every write route (401) - not
83
+ // usable for editing at all. Generated fresh per scaffold, printed
84
+ // once, never written to disk - matches every other write-capable
85
+ // identity in this codebase never depending on a value the operator
86
+ // could lose or that leaks into version control.
87
+ const token = generateToken();
88
+ writeFileSync(join(vhostDir, 'site.config.json'), JSON.stringify({
89
+ tokens: [{ hash: token.hash, scopes: ['content', 'theme', 'media'] }],
90
+ }, null, 2));
91
+ const packageName = sanitisePackageName(basename(targetDir));
92
+ writeFileSync(join(vhostDir, 'package.json'), JSON.stringify({
93
+ name: packageName,
94
+ version: '0.0.0',
95
+ private: true,
96
+ type: 'module',
97
+ dependencies: {
98
+ // Pinned exact, never a ^range - at v0.x even a minor bump
99
+ // can be breaking (build plan's own word is "pinned").
100
+ '@o-a/cms-agent': readAgentVersion(),
101
+ },
102
+ }, null, 2));
103
+ writeFileSync(join(vhostDir, 'server.js'), SERVER_JS);
104
+ // runStartupChecks hard-fails not-a-git-repo otherwise - a scaffold
105
+ // without a real git repo cannot boot at all.
106
+ execFileSync('git', ['init', '--quiet'], { cwd: targetDir });
107
+ commitPaths(targetDir, ['theme', 'content', 'vhost', '.gitignore', '.dockerignore'], 'chore: initial scaffold', CHECKPOINT_AUTHOR);
108
+ return { raw: token.raw };
109
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import { MintTokenError, mintToken, parseScopes } from "./mint-token.js";
4
+ const targetArg = process.argv[2];
5
+ const scopesFlagIndex = process.argv.indexOf('--scopes');
6
+ const scopesArg = scopesFlagIndex !== -1 ? process.argv[scopesFlagIndex + 1] : undefined;
7
+ if (!targetArg) {
8
+ console.error('Usage: mint-token <site-directory> [--scopes content,theme,media]');
9
+ process.exit(1);
10
+ }
11
+ try {
12
+ const scopes = parseScopes(scopesArg);
13
+ const siteDir = resolve(process.cwd(), targetArg);
14
+ const { raw } = mintToken(siteDir, scopes);
15
+ console.log('New API token (save this now - it will not be shown again):');
16
+ console.log(` ${raw}`);
17
+ console.log('');
18
+ console.log(`Scopes: ${scopes.join(', ')}`);
19
+ }
20
+ catch (error) {
21
+ if (error instanceof MintTokenError) {
22
+ console.error(error.message);
23
+ process.exit(1);
24
+ }
25
+ throw error;
26
+ }
@@ -0,0 +1,7 @@
1
+ import type { Scope } from '../server-config.ts';
2
+ export declare class MintTokenError extends Error {
3
+ }
4
+ export declare function parseScopes(raw: string | undefined): Scope[];
5
+ export declare function mintToken(siteDir: string, scopes: Scope[]): {
6
+ raw: string;
7
+ };
@@ -0,0 +1,49 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { CHECKPOINT_AUTHOR } from "../services/checkpoint.js";
4
+ import { commitPaths } from "../services/git.js";
5
+ import { generateToken } from "./generate-site.js";
6
+ export class MintTokenError extends Error {
7
+ }
8
+ const VALID_SCOPES = new Set(['content', 'theme', 'media']);
9
+ export function parseScopes(raw) {
10
+ if (!raw) {
11
+ return ['content', 'theme', 'media'];
12
+ }
13
+ const scopes = raw.split(',').map((scope) => scope.trim());
14
+ for (const scope of scopes) {
15
+ if (!VALID_SCOPES.has(scope)) {
16
+ throw new MintTokenError(`Unknown scope "${scope}" - valid scopes are content, theme, media`);
17
+ }
18
+ }
19
+ return scopes;
20
+ }
21
+ // A standalone CLI operation against an operator-supplied site
22
+ // directory, same category as scaffoldSite - not a web request's
23
+ // :path, so sanitisePath's traversal concern doesn't apply here.
24
+ export function mintToken(siteDir, scopes) {
25
+ const configPath = join(siteDir, 'vhost', 'site.config.json');
26
+ let raw;
27
+ try {
28
+ raw = readFileSync(configPath, 'utf-8');
29
+ }
30
+ catch (error) {
31
+ throw new MintTokenError(`Could not read ${configPath} - is "${siteDir}" a real site created by create-site?`, {
32
+ cause: error,
33
+ });
34
+ }
35
+ let config;
36
+ try {
37
+ config = JSON.parse(raw);
38
+ }
39
+ catch (error) {
40
+ throw new MintTokenError(`${configPath} is not valid JSON`, { cause: error });
41
+ }
42
+ const token = generateToken();
43
+ const tokens = Array.isArray(config.tokens) ? config.tokens : [];
44
+ tokens.push({ hash: token.hash, scopes });
45
+ config.tokens = tokens;
46
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
47
+ commitPaths(siteDir, ['vhost/site.config.json'], 'chore: mint a new API token', CHECKPOINT_AUTHOR);
48
+ return { raw: token.raw };
49
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "items": [
4
+ { "label": "About", "url": "/about" },
5
+ { "label": "Careers", "url": "/careers" },
6
+ { "label": "Contact", "url": "/contact" },
7
+ { "label": "GitHub", "url": "https://github.com/granite-cms/granite" }
8
+ ]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "items": [
4
+ { "label": "Features", "url": "#features" },
5
+ { "label": "Pricing", "url": "#pricing" },
6
+ { "label": "Changelog", "url": "/changelog" },
7
+ { "label": "Docs", "url": "/docs" }
8
+ ]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "items": [
4
+ { "label": "Documentation", "url": "/docs" },
5
+ { "label": "Theme authoring guide", "url": "/docs/themes" },
6
+ { "label": "Blog", "url": "/blog" },
7
+ { "label": "Community", "url": "https://discord.gg/granitecms" }
8
+ ]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "items": [
4
+ { "label": "Features", "url": "#features" },
5
+ { "label": "How it works", "url": "#how-it-works" },
6
+ { "label": "Pricing", "url": "#pricing" },
7
+ { "label": "FAQ", "url": "#faq" }
8
+ ]
9
+ }