@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.
- package/LICENSE +105 -0
- package/README.md +132 -0
- package/dist/boot.d.ts +15 -0
- package/dist/boot.js +33 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +45 -0
- package/dist/create-site/cli.d.ts +2 -0
- package/dist/create-site/cli.js +31 -0
- package/dist/create-site/generate-site.d.ts +9 -0
- package/dist/create-site/generate-site.js +109 -0
- package/dist/create-site/mint-token-cli.d.ts +2 -0
- package/dist/create-site/mint-token-cli.js +26 -0
- package/dist/create-site/mint-token.d.ts +7 -0
- package/dist/create-site/mint-token.js +49 -0
- package/dist/create-site/template/content/menus/footerCompany.json +9 -0
- package/dist/create-site/template/content/menus/footerProduct.json +9 -0
- package/dist/create-site/template/content/menus/footerResources.json +9 -0
- package/dist/create-site/template/content/menus/main.json +9 -0
- package/dist/create-site/template/content/pages/404.json +22 -0
- package/dist/create-site/template/content/pages/about/careers.json +21 -0
- package/dist/create-site/template/content/pages/about/team.json +21 -0
- package/dist/create-site/template/content/pages/about.json +21 -0
- package/dist/create-site/template/content/pages/docs/deployment.json +21 -0
- package/dist/create-site/template/content/pages/docs/getting-started/quickstart.json +21 -0
- package/dist/create-site/template/content/pages/docs/getting-started.json +23 -0
- package/dist/create-site/template/content/pages/docs.json +21 -0
- package/dist/create-site/template/content/pages/index.json +181 -0
- package/dist/create-site/template/content/redirects.json +4 -0
- package/dist/create-site/template/dockerignore +6 -0
- package/dist/create-site/template/gitignore +11 -0
- package/dist/create-site/template/theme/assets/site.js +49 -0
- package/dist/create-site/template/theme/assets/style.css +1153 -0
- package/dist/create-site/template/theme/blocks/button.liquid +21 -0
- package/dist/create-site/template/theme/blocks/comparison-row.liquid +19 -0
- package/dist/create-site/template/theme/blocks/faq-item.liquid +22 -0
- package/dist/create-site/template/theme/blocks/feature-card.liquid +23 -0
- package/dist/create-site/template/theme/blocks/logo-mark.liquid +13 -0
- package/dist/create-site/template/theme/blocks/pricing-tier.liquid +42 -0
- package/dist/create-site/template/theme/blocks/step-card.liquid +19 -0
- package/dist/create-site/template/theme/blocks/testimonial-card.liquid +25 -0
- package/dist/create-site/template/theme/layouts/theme.liquid +99 -0
- package/dist/create-site/template/theme/sections/comparison.liquid +41 -0
- package/dist/create-site/template/theme/sections/cta-banner.liquid +27 -0
- package/dist/create-site/template/theme/sections/faq.liquid +27 -0
- package/dist/create-site/template/theme/sections/feature-grid.liquid +27 -0
- package/dist/create-site/template/theme/sections/hero.liquid +36 -0
- package/dist/create-site/template/theme/sections/how-it-works.liquid +27 -0
- package/dist/create-site/template/theme/sections/pricing.liquid +27 -0
- package/dist/create-site/template/theme/sections/social-proof.liquid +21 -0
- package/dist/create-site/template/theme/sections/testimonials.liquid +27 -0
- package/dist/create-site/template/theme/snippets/icon.liquid +24 -0
- package/dist/create-site/template/theme/snippets/logo.liquid +7 -0
- package/dist/create-site/template/theme/snippets/site-name.liquid +1 -0
- package/dist/create-site/template/vhost/Dockerfile +33 -0
- package/dist/create-site/template/vhost/docker-entrypoint.sh +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/media/drivers/driver.d.ts +11 -0
- package/dist/media/drivers/driver.js +1 -0
- package/dist/media/drivers/local-fs-driver.d.ts +3 -0
- package/dist/media/drivers/local-fs-driver.js +81 -0
- package/dist/media/filename.d.ts +1 -0
- package/dist/media/filename.js +31 -0
- package/dist/media/manage-media.d.ts +19 -0
- package/dist/media/manage-media.js +45 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +49 -0
- package/dist/renderer/engine.d.ts +2 -0
- package/dist/renderer/engine.js +48 -0
- package/dist/renderer/render-page.d.ts +28 -0
- package/dist/renderer/render-page.js +143 -0
- package/dist/renderer/theme-templates.d.ts +7 -0
- package/dist/renderer/theme-templates.js +82 -0
- package/dist/routes/assets.d.ts +6 -0
- package/dist/routes/assets.js +30 -0
- package/dist/routes/batch.d.ts +10 -0
- package/dist/routes/batch.js +148 -0
- package/dist/routes/capabilities.d.ts +5 -0
- package/dist/routes/capabilities.js +27 -0
- package/dist/routes/content.d.ts +8 -0
- package/dist/routes/content.js +166 -0
- package/dist/routes/drafts.d.ts +10 -0
- package/dist/routes/drafts.js +73 -0
- package/dist/routes/git.d.ts +8 -0
- package/dist/routes/git.js +158 -0
- package/dist/routes/index.d.ts +19 -0
- package/dist/routes/index.js +84 -0
- package/dist/routes/media-public.d.ts +5 -0
- package/dist/routes/media-public.js +48 -0
- package/dist/routes/media.d.ts +9 -0
- package/dist/routes/media.js +84 -0
- package/dist/routes/menus.d.ts +8 -0
- package/dist/routes/menus.js +77 -0
- package/dist/routes/preview-revision.d.ts +13 -0
- package/dist/routes/preview-revision.js +109 -0
- package/dist/routes/preview.d.ts +13 -0
- package/dist/routes/preview.js +52 -0
- package/dist/routes/public.d.ts +11 -0
- package/dist/routes/public.js +117 -0
- package/dist/routes/publish.d.ts +10 -0
- package/dist/routes/publish.js +105 -0
- package/dist/routes/redirects.d.ts +8 -0
- package/dist/routes/redirects.js +132 -0
- package/dist/routes/search.d.ts +8 -0
- package/dist/routes/search.js +11 -0
- package/dist/routes/sitemap.d.ts +6 -0
- package/dist/routes/sitemap.js +79 -0
- package/dist/routes/theme.d.ts +10 -0
- package/dist/routes/theme.js +17 -0
- package/dist/schemas/instance.schema.json +17 -0
- package/dist/schemas/menu.schema.json +23 -0
- package/dist/schemas/page.schema.json +20 -0
- package/dist/schemas/post.schema.json +25 -0
- package/dist/schemas/redirects.schema.json +24 -0
- package/dist/search/drivers/driver.d.ts +9 -0
- package/dist/search/drivers/driver.js +1 -0
- package/dist/search/drivers/node-sqlite-driver.d.ts +3 -0
- package/dist/search/drivers/node-sqlite-driver.js +19 -0
- package/dist/search/query-index.d.ts +5 -0
- package/dist/search/query-index.js +21 -0
- package/dist/search/rebuild-index.d.ts +2 -0
- package/dist/search/rebuild-index.js +111 -0
- package/dist/server-config.d.ts +22 -0
- package/dist/server-config.js +196 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +163 -0
- package/dist/services/batch.d.ts +35 -0
- package/dist/services/batch.js +136 -0
- package/dist/services/checkpoint.d.ts +4 -0
- package/dist/services/checkpoint.js +45 -0
- package/dist/services/content-read.d.ts +27 -0
- package/dist/services/content-read.js +156 -0
- package/dist/services/delete-content.d.ts +12 -0
- package/dist/services/delete-content.js +174 -0
- package/dist/services/dev-tunnel.d.ts +5 -0
- package/dist/services/dev-tunnel.js +5 -0
- package/dist/services/drafts.d.ts +15 -0
- package/dist/services/drafts.js +155 -0
- package/dist/services/etag.d.ts +2 -0
- package/dist/services/etag.js +30 -0
- package/dist/services/fs-walk.d.ts +1 -0
- package/dist/services/fs-walk.js +26 -0
- package/dist/services/git-commit.d.ts +3 -0
- package/dist/services/git-commit.js +12 -0
- package/dist/services/git-history.d.ts +28 -0
- package/dist/services/git-history.js +131 -0
- package/dist/services/git-revert.d.ts +12 -0
- package/dist/services/git-revert.js +140 -0
- package/dist/services/git.d.ts +16 -0
- package/dist/services/git.js +127 -0
- package/dist/services/interval-job.d.ts +4 -0
- package/dist/services/interval-job.js +9 -0
- package/dist/services/ip-allowlist.d.ts +7 -0
- package/dist/services/ip-allowlist.js +31 -0
- package/dist/services/manage-menus.d.ts +10 -0
- package/dist/services/manage-menus.js +84 -0
- package/dist/services/manage-redirects.d.ts +25 -0
- package/dist/services/manage-redirects.js +166 -0
- package/dist/services/menus.d.ts +8 -0
- package/dist/services/menus.js +42 -0
- package/dist/services/migration-runner.d.ts +13 -0
- package/dist/services/migration-runner.js +123 -0
- package/dist/services/mime-types.d.ts +3 -0
- package/dist/services/mime-types.js +36 -0
- package/dist/services/move.d.ts +16 -0
- package/dist/services/move.js +175 -0
- package/dist/services/path-safety.d.ts +6 -0
- package/dist/services/path-safety.js +112 -0
- package/dist/services/post-urls.d.ts +3 -0
- package/dist/services/post-urls.js +27 -0
- package/dist/services/prepared-operation.d.ts +4 -0
- package/dist/services/prepared-operation.js +1 -0
- package/dist/services/publish.d.ts +14 -0
- package/dist/services/publish.js +254 -0
- package/dist/services/rate-limit-config.d.ts +9 -0
- package/dist/services/rate-limit-config.js +20 -0
- package/dist/services/redirects.d.ts +27 -0
- package/dist/services/redirects.js +175 -0
- package/dist/services/resolve-blog-url.d.ts +11 -0
- package/dist/services/resolve-blog-url.js +31 -0
- package/dist/services/resolve-url.d.ts +11 -0
- package/dist/services/resolve-url.js +20 -0
- package/dist/services/startup-checks.d.ts +10 -0
- package/dist/services/startup-checks.js +81 -0
- package/dist/services/static-file.d.ts +7 -0
- package/dist/services/static-file.js +37 -0
- package/dist/services/theme-component-file.d.ts +5 -0
- package/dist/services/theme-component-file.js +32 -0
- package/dist/services/theme-page-templates.d.ts +7 -0
- package/dist/services/theme-page-templates.js +53 -0
- package/dist/services/theme-schemas.d.ts +2 -0
- package/dist/services/theme-schemas.js +63 -0
- package/dist/services/token-auth.d.ts +10 -0
- package/dist/services/token-auth.js +64 -0
- package/dist/services/urls.d.ts +2 -0
- package/dist/services/urls.js +20 -0
- package/dist/services/validation.d.ts +24 -0
- package/dist/services/validation.js +158 -0
- package/dist/services/write-queue.d.ts +1 -0
- package/dist/services/write-queue.js +14 -0
- 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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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,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,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,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
|
+
}
|