create-boject-cms 0.0.1-rc.1

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 ADDED
@@ -0,0 +1,95 @@
1
+ Business Source License 1.1
2
+
3
+ License text copyright © 2017 MariaDB Corporation Ab, All Rights Reserved.
4
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
5
+
6
+ -----------------------------------------------------------------------------
7
+
8
+ Parameters
9
+
10
+ Licensor: Boject Ltd
11
+
12
+ Licensed Work: boject-cms 0.0.1-rc.1
13
+ The Licensed Work is © 2026 Boject Ltd.
14
+
15
+ Additional Use Grant: You may make production use of the Licensed Work,
16
+ provided such production use does not include offering
17
+ the Licensed Work to third parties on a hosted, managed,
18
+ or embedded basis that competes with boject's products or
19
+ services.
20
+
21
+ Change Date: 2030-07-07
22
+
23
+ Change License: Apache License, Version 2.0
24
+
25
+ For information about alternative licensing arrangements for the Licensed Work,
26
+ please contact licensing@boject.com.
27
+
28
+ -----------------------------------------------------------------------------
29
+
30
+ Terms
31
+
32
+ The Licensor hereby grants you the right to copy, modify, create derivative
33
+ works, redistribute, and make non-production use of the Licensed Work. The
34
+ Licensor may make an Additional Use Grant, above, permitting limited
35
+ production use.
36
+
37
+ Effective on the Change Date, or the fourth anniversary of the first publicly
38
+ available distribution of a specific version of the Licensed Work under this
39
+ License, whichever comes first, the Licensor hereby grants you rights under
40
+ the terms of the Change License, and the rights granted in the paragraph
41
+ above terminate.
42
+
43
+ If your use of the Licensed Work does not comply with the requirements
44
+ currently in effect as described in this License, you must purchase a
45
+ commercial license from the Licensor, its affiliated entities, or authorized
46
+ resellers, or you must refrain from using the Licensed Work.
47
+
48
+ All copies of the original and modified Licensed Work, and derivative works
49
+ of the Licensed Work, are subject to this License. This License applies
50
+ separately for each version of the Licensed Work and the Change Date may vary
51
+ for each version of the Licensed Work released by Licensor.
52
+
53
+ You must conspicuously display this License on each original or modified copy
54
+ of the Licensed Work. If you receive the Licensed Work in original or
55
+ modified form from a third party, the terms and conditions set forth in this
56
+ License apply to your use of that work.
57
+
58
+ Any use of the Licensed Work in violation of this License will automatically
59
+ terminate your rights under this License for the current and all other
60
+ versions of the Licensed Work.
61
+
62
+ This License does not grant you any right in any trademark or logo of
63
+ Licensor or its affiliates (provided that you may use a trademark or logo of
64
+ Licensor as expressly required by this License).
65
+
66
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
67
+ AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
68
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
69
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
70
+ TITLE.
71
+
72
+ MariaDB hereby grants you permission to use this License’s text to license
73
+ your works, and to refer to it using the trademark “Business Source License”,
74
+ as long as you comply with the Covenants of Licensor below.
75
+
76
+ Covenants of Licensor
77
+
78
+ In consideration of the right to use this License’s text and the “Business
79
+ Source License” name and trademark, Licensor covenants to MariaDB, and to all
80
+ other recipients of the licensed work to be provided by Licensor:
81
+
82
+ 1. To specify as the Change License the GPL Version 2.0 or any later version,
83
+ or a license that is compatible with GPL Version 2.0 or a later version,
84
+ where “compatible” means that software provided under the Change License can
85
+ be included in a program with software provided under GPL Version 2.0 or a
86
+ later version. Licensor may specify additional Change Licenses without
87
+ limitation.
88
+
89
+ 2. To either: (a) specify an additional grant of rights to use that does not
90
+ impose any additional restriction on the right granted in this License, as
91
+ the Additional Use Grant; or (b) insert the text “None”.
92
+
93
+ 3. To specify a Change Date.
94
+
95
+ 4. Not to modify this License in any other way.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # create-boject-cms
2
+
3
+ [![npm](https://img.shields.io/npm/v/create-boject-cms)](https://www.npmjs.com/package/create-boject-cms)
4
+ [![License: BUSL-1.1](https://img.shields.io/badge/license-BUSL--1.1-blue.svg)](./LICENSE)
5
+
6
+ Scaffold a new [boject-cms](https://github.com/bojectify/boject-cms) project — a Docker Compose project pinned to a specific CMS image, with a starter content schema, scaffolded `.env`, and the `.boject.config.json` for the [@boject/cli](https://www.npmjs.com/package/@boject/cli) commands.
7
+
8
+ ## Usage
9
+
10
+ ```bash
11
+ pnpm create boject-cms my-site
12
+ # or
13
+ npx create-boject-cms my-site
14
+ ```
15
+
16
+ You'll be prompted to pick a starter:
17
+
18
+ | Starter | What it ships |
19
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | **base** | 8 universal content types (Image, Tag, Author, Article, Page, SiteSettings, Navigation, NavigationItem) plus one SiteSettings seed entry. |
21
+ | **sport** | base + Team, Club, Competition, Season, Fixture, Player. |
22
+ | **rugby** | sport + Position + a patched Player. |
23
+ | **none** | No starter — empty `content-types/schema.boject.json`. |
24
+
25
+ Non-interactive use:
26
+
27
+ ```bash
28
+ pnpm create boject-cms my-site --starter base
29
+ pnpm create boject-cms my-site --starter base --image ghcr.io/bojectify/boject-cms:1.4.2
30
+ pnpm create boject-cms my-site --force # scaffold into a non-empty directory
31
+ ```
32
+
33
+ ## What gets scaffolded
34
+
35
+ ```
36
+ my-site/
37
+ ├── .boject.config.json # @boject/cli config (cms.url + schema.path)
38
+ ├── .env # secrets + runtime env vars
39
+ ├── .gitignore
40
+ ├── README.md # project-specific quickstart
41
+ ├── content-types/
42
+ │ └── schema.boject.json # the chosen starter's bundle (edit + commit)
43
+ ├── docker-compose.yml # cms + db services, image pinned to a specific tag
44
+ ├── package.json # one upgrade script
45
+ └── starters/
46
+ └── <starter>.boject.json # the starter bundle (mounted into the container)
47
+ ```
48
+
49
+ ## First boot
50
+
51
+ ```bash
52
+ cd my-site
53
+ docker compose up -d
54
+ ```
55
+
56
+ The container's entrypoint:
57
+
58
+ 1. Waits for PostgreSQL.
59
+ 2. Runs Prisma migrations.
60
+ 3. Seeds an admin user from `BOJECT_ADMIN_EMAIL` + `BOJECT_ADMIN_PASSWORD` (only if the User table is empty).
61
+ 4. Imports `BOJECT_INITIAL_STARTER` (`/starters/<starter>.boject.json`) into an empty CMS — schema + entries.
62
+ 5. Runs the schema-as-code applier against `BOJECT_SCHEMA_DIR` (`/app/content-types`) — converges the schema on every boot.
63
+ 6. Starts Nuxt.
64
+
65
+ Once the container is healthy, log in at http://localhost:4000/login. The admin email is `admin@local`; the password lives in `.env` under `BOJECT_ADMIN_PASSWORD`.
66
+
67
+ ## Scaffolded `.env`
68
+
69
+ | Variable | Purpose |
70
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
71
+ | `DATABASE_URL` | PostgreSQL connection string (points at the `db` service). |
72
+ | `NUXT_SESSION_PASSWORD` | 32-byte random session encryption key. |
73
+ | `BOJECT_ADMIN_EMAIL` | Initial admin (`admin@local`). |
74
+ | `BOJECT_ADMIN_PASSWORD` | 16-byte random initial admin password. |
75
+ | `STORAGE_DRIVER` | `local` (default), `s3`, or `r2`. |
76
+ | `BOJECT_SCHEMA_DIR` | `/app/content-types` — the entrypoint applies any `*.boject.json` here on every boot. |
77
+ | `BOJECT_INITIAL_STARTER` | First-boot starter path (`/starters/<starter>.boject.json`). |
78
+ | `# BOJECT_SCHEMA_READONLY=true` | Commented. Set on production / staging to disable schema editing in the UI. |
79
+ | `# BOJECT_ALLOW_DESTRUCTIVE_SCHEMA=true` | Commented. Set on environments where the entrypoint applier should remove types/fields the bundle drops. |
80
+ | `# BOJECT_API_KEY=` | Commented. Set when running the [`boject schema *`](https://www.npmjs.com/package/@boject/cli) commands locally. |
81
+
82
+ ## Schema-as-code workflow
83
+
84
+ After scaffold, your content types live in `content-types/schema.boject.json`. The intended loop:
85
+
86
+ ```bash
87
+ # 1. Edit content types in the CMS UI in development.
88
+
89
+ # 2. Pull the live schema into your project.
90
+ pnpm exec boject schema pull
91
+
92
+ # 3. Review the diff and commit.
93
+ git diff content-types/schema.boject.json
94
+ git commit -am "Add 'publishedOn' field to Article"
95
+
96
+ # 4. Deploy. The container's entrypoint applies the bundle on boot.
97
+ git push
98
+ ```
99
+
100
+ The scaffolded `README.md` explains this for the project's developers; this file (the scaffolder's own README) covers the meta-tooling.
101
+
102
+ Destructive changes (removing types or fields) are blocked by default — set `BOJECT_ALLOW_DESTRUCTIVE_SCHEMA=true` in `.env` to allow the entrypoint applier to act on removals.
103
+
104
+ ## Upgrading the CMS image
105
+
106
+ After scaffolding, the project's `package.json` exposes a single script:
107
+
108
+ ```bash
109
+ pnpm upgrade
110
+ ```
111
+
112
+ This runs `boject upgrade`, which finds the latest semver tag in the registry, rewrites `docker-compose.yml`, pulls the new image, restarts, and waits for the health endpoint.
113
+
114
+ ```bash
115
+ pnpm exec boject upgrade --check # exit 1 if upgrade available
116
+ pnpm exec boject upgrade --to 1.4.2 # specific version
117
+ ```
118
+
119
+ ## See also
120
+
121
+ - The CMS itself: [`boject-cms`](https://github.com/bojectify/boject-cms)
122
+ - The CLI: [`@boject/cli`](https://www.npmjs.com/package/@boject/cli)
123
+ - Starter bundles: [`starters/`](https://github.com/bojectify/boject-cms/tree/main/starters)
124
+
125
+ ## License
126
+
127
+ Source-available under the [Business Source License 1.1](./LICENSE); each published version converts to Apache-2.0 four years after its release. See the [repository README](https://github.com/bojectify/boject-cms#license) for the plain-language summary.
package/dist/index.js ADDED
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { fileURLToPath } from "url";
5
+ import { dirname, join as join2, resolve } from "path";
6
+ import { parseArgs } from "util";
7
+
8
+ // src/prompts.ts
9
+ import { isCancel, select } from "@clack/prompts";
10
+ var CHOICES = ["base", "sport", "rugby", "none"];
11
+ function isValidChoice(value) {
12
+ return CHOICES.includes(value);
13
+ }
14
+ async function resolveStarter({
15
+ flag,
16
+ isTTY
17
+ }) {
18
+ if (flag !== void 0) {
19
+ if (!isValidChoice(flag)) {
20
+ throw new Error(`--starter must be one of: ${CHOICES.join(", ")}`);
21
+ }
22
+ return flag;
23
+ }
24
+ if (!isTTY) {
25
+ throw new Error(
26
+ "Non-interactive shell detected. Pass --starter <base|sport|rugby|none>."
27
+ );
28
+ }
29
+ const response = await select({
30
+ message: "Which starter?",
31
+ options: [
32
+ { value: "base", label: "Base (8 universal content types)" },
33
+ {
34
+ value: "sport",
35
+ label: "Sport (base + team/club/competition/fixture/player)"
36
+ },
37
+ { value: "rugby", label: "Rugby (sport + Position + patched Player)" },
38
+ { value: "none", label: "None (empty database)" }
39
+ ],
40
+ initialValue: "base"
41
+ });
42
+ if (isCancel(response)) {
43
+ throw new Error("Scaffold cancelled by user.");
44
+ }
45
+ return response;
46
+ }
47
+
48
+ // src/version.ts
49
+ var IMAGE_TAG = "ghcr.io/bojectify/boject-cms:0.0.1-rc.1";
50
+
51
+ // src/writeProject.ts
52
+ import { copyFile, mkdir, readdir, writeFile } from "fs/promises";
53
+ import { basename, join } from "path";
54
+
55
+ // src/templates/dockerCompose.ts
56
+ function renderDockerCompose({
57
+ imageTag,
58
+ starter
59
+ }) {
60
+ const starterMount = starter === "none" ? "" : ` - ./starters:/starters:ro
61
+ `;
62
+ return `services:
63
+ cms:
64
+ image: ${imageTag}
65
+ restart: unless-stopped
66
+ ports:
67
+ - '4000:3000'
68
+ env_file:
69
+ - .env
70
+ depends_on:
71
+ - db
72
+ volumes:
73
+ - storage:/app/storage
74
+ - ./content-types:/app/content-types:ro
75
+ ${starterMount} db:
76
+ image: postgres:17
77
+ restart: unless-stopped
78
+ environment:
79
+ POSTGRES_USER: boject
80
+ POSTGRES_PASSWORD: boject
81
+ POSTGRES_DB: boject
82
+ volumes:
83
+ - pgdata:/var/lib/postgresql/data
84
+
85
+ volumes:
86
+ pgdata:
87
+ storage:
88
+ `;
89
+ }
90
+
91
+ // src/templates/envFile.ts
92
+ function renderEnvFile({
93
+ sessionPassword,
94
+ adminPassword,
95
+ starter
96
+ }) {
97
+ const lines = [
98
+ "DATABASE_URL=postgresql://boject:boject@db:5432/boject",
99
+ `NUXT_SESSION_PASSWORD=${sessionPassword}`,
100
+ "BOJECT_ADMIN_EMAIL=admin@local",
101
+ `BOJECT_ADMIN_PASSWORD=${adminPassword}`,
102
+ "STORAGE_DRIVER=local",
103
+ "BOJECT_SCHEMA_DIR=/app/content-types"
104
+ ];
105
+ if (starter !== "none") {
106
+ lines.push(`BOJECT_INITIAL_STARTER=/starters/${starter}.boject.json`);
107
+ }
108
+ lines.push(
109
+ "",
110
+ '# Set to "true" on production / staging to disable schema editing in the UI.',
111
+ "# Schema changes should flow from git on locked environments.",
112
+ "# BOJECT_SCHEMA_READONLY=true",
113
+ "",
114
+ "# Allow destructive schema changes (removing content types or fields)",
115
+ "# during the every-boot apply. Default off \u2014 only additive changes apply",
116
+ "# automatically. Flip this on for environments where bundle removals are",
117
+ "# expected to take effect.",
118
+ "# BOJECT_ALLOW_DESTRUCTIVE_SCHEMA=true",
119
+ "",
120
+ "# Set when using the boject CLI (`boject schema pull/apply/check`).",
121
+ "# Create one with: pnpm apikey:create cli --scopes schema:read,schema:write",
122
+ "# BOJECT_API_KEY="
123
+ );
124
+ return lines.join("\n") + "\n";
125
+ }
126
+
127
+ // src/templates/packageJson.ts
128
+ function renderPackageJson({ name }) {
129
+ const pkg = {
130
+ name,
131
+ version: "0.1.0",
132
+ private: true,
133
+ scripts: {
134
+ start: "docker compose up -d",
135
+ stop: "docker compose down",
136
+ logs: "docker compose logs -f cms",
137
+ upgrade: "npx @boject/cli@latest upgrade"
138
+ }
139
+ };
140
+ return JSON.stringify(pkg, null, 2) + "\n";
141
+ }
142
+
143
+ // src/templates/readme.ts
144
+ function renderReadme({ starter, adminEmail }) {
145
+ const starterLine = starter === "none" ? "" : `The \`${starter}\` starter bundle will be imported on first boot.
146
+
147
+ `;
148
+ return `# boject-cms
149
+
150
+ A new boject-cms project scaffolded by \`create-boject-cms\`.
151
+
152
+ ## Start the CMS
153
+
154
+ \`\`\`bash
155
+ docker compose up -d
156
+ \`\`\`
157
+
158
+ ${starterLine}Once the container is healthy, log in at http://localhost:4000/login with:
159
+
160
+ - Email: \`${adminEmail}\`
161
+ - Password: see \`BOJECT_ADMIN_PASSWORD\` in \`.env\`
162
+
163
+ ## Content types
164
+
165
+ Your content type schema lives in \`content-types/schema.boject.json\` and is
166
+ applied to the database on every container boot. Edit it via the CMS UI in
167
+ development; commit the file alongside your code so production deploys converge
168
+ to the same schema. Destructive changes (removing types or fields) are blocked
169
+ by default \u2014 set \`BOJECT_ALLOW_DESTRUCTIVE_SCHEMA=true\` in \`.env\` to allow them.
170
+
171
+ ## Stop the CMS
172
+
173
+ \`\`\`bash
174
+ docker compose down
175
+ \`\`\`
176
+
177
+ ## Upgrade the CMS image
178
+
179
+ \`\`\`bash
180
+ pnpm upgrade
181
+ \`\`\`
182
+
183
+ This runs \`npx @boject/cli@latest upgrade\` to rewrite the pinned image tag in \`docker-compose.yml\` and restart the container.
184
+ `;
185
+ }
186
+
187
+ // src/templates/gitignore.ts
188
+ var GITIGNORE = `.env
189
+ storage/
190
+ pgdata/
191
+ `;
192
+
193
+ // src/templates/contentTypes.ts
194
+ function renderContentTypesBundle({
195
+ starter
196
+ }) {
197
+ if (starter === "none") {
198
+ const stub = {
199
+ version: 2,
200
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
201
+ portable: true,
202
+ contentTypes: []
203
+ };
204
+ return { kind: "content", content: JSON.stringify(stub, null, 2) + "\n" };
205
+ }
206
+ return { kind: "copy", sourceFilename: `${starter}.boject.json` };
207
+ }
208
+
209
+ // src/templates/bojectConfig.ts
210
+ function renderBojectConfig() {
211
+ const obj = {
212
+ cms: { url: "http://localhost:4000" },
213
+ schema: { path: "content-types/schema.boject.json" }
214
+ };
215
+ return JSON.stringify(obj, null, 2) + "\n";
216
+ }
217
+
218
+ // src/projectName.ts
219
+ function sanitiseProjectName(raw) {
220
+ const lowered = raw.toLowerCase();
221
+ const replaced = lowered.replace(/[^a-z0-9._]+/g, "-");
222
+ const trimmed = replaced.replace(/^-+|-+$/g, "");
223
+ return trimmed.length > 0 ? trimmed : "boject-site";
224
+ }
225
+
226
+ // src/secrets.ts
227
+ import { randomBytes } from "crypto";
228
+ function generateSessionPassword() {
229
+ return randomBytes(32).toString("base64");
230
+ }
231
+ function generateAdminPassword() {
232
+ return randomBytes(16).toString("base64");
233
+ }
234
+
235
+ // src/writeProject.ts
236
+ async function writeProject({
237
+ targetDir,
238
+ starter,
239
+ imageTag,
240
+ force,
241
+ startersSourceDir
242
+ }) {
243
+ await mkdir(targetDir, { recursive: true });
244
+ const existing = await readdir(targetDir);
245
+ if (existing.length > 0 && !force) {
246
+ throw new Error(
247
+ `Target directory "${targetDir}" is not empty. Pass --force to scaffold anyway.`
248
+ );
249
+ }
250
+ const sessionPassword = generateSessionPassword();
251
+ const adminPassword = generateAdminPassword();
252
+ const adminEmail = "admin@local";
253
+ const projectName = sanitiseProjectName(basename(targetDir));
254
+ await writeFile(
255
+ join(targetDir, "docker-compose.yml"),
256
+ renderDockerCompose({ imageTag, starter })
257
+ );
258
+ await writeFile(
259
+ join(targetDir, ".env"),
260
+ renderEnvFile({ sessionPassword, adminPassword, starter })
261
+ );
262
+ await writeFile(
263
+ join(targetDir, "package.json"),
264
+ renderPackageJson({ name: projectName })
265
+ );
266
+ await writeFile(join(targetDir, ".gitignore"), GITIGNORE);
267
+ await writeFile(
268
+ join(targetDir, "README.md"),
269
+ renderReadme({ starter, adminEmail })
270
+ );
271
+ if (starter !== "none") {
272
+ const startersTarget = join(targetDir, "starters");
273
+ await mkdir(startersTarget, { recursive: true });
274
+ const source = join(startersSourceDir, `${starter}.boject.json`);
275
+ const dest = join(startersTarget, `${starter}.boject.json`);
276
+ await copyFile(source, dest);
277
+ }
278
+ const contentTypesTarget = join(targetDir, "content-types");
279
+ await mkdir(contentTypesTarget, { recursive: true });
280
+ const bundleResult = renderContentTypesBundle({ starter });
281
+ if (bundleResult.kind === "content") {
282
+ await writeFile(
283
+ join(contentTypesTarget, "schema.boject.json"),
284
+ bundleResult.content
285
+ );
286
+ } else {
287
+ await copyFile(
288
+ join(startersSourceDir, bundleResult.sourceFilename),
289
+ join(contentTypesTarget, "schema.boject.json")
290
+ );
291
+ }
292
+ await writeFile(join(targetDir, ".boject.config.json"), renderBojectConfig());
293
+ return { adminEmail, adminPassword };
294
+ }
295
+
296
+ // src/index.ts
297
+ function parseCli(argv) {
298
+ const { values, positionals } = parseArgs({
299
+ args: argv,
300
+ allowPositionals: true,
301
+ options: {
302
+ force: { type: "boolean", default: false },
303
+ starter: { type: "string" },
304
+ image: { type: "string" }
305
+ }
306
+ });
307
+ if (positionals.length !== 1) {
308
+ process.stderr.write(
309
+ "Usage: create-boject-cms <target-dir> [--force] [--starter <name>] [--image <tag>]\n"
310
+ );
311
+ process.exit(1);
312
+ }
313
+ return {
314
+ targetDir: resolve(positionals[0]),
315
+ force: values.force === true,
316
+ starter: values.starter,
317
+ imageTag: values.image ?? IMAGE_TAG
318
+ };
319
+ }
320
+ function resolveStartersSourceDir() {
321
+ const here = dirname(fileURLToPath(import.meta.url));
322
+ return join2(here, "starters");
323
+ }
324
+ async function main() {
325
+ const {
326
+ targetDir,
327
+ force,
328
+ starter: starterFlag,
329
+ imageTag
330
+ } = parseCli(process.argv.slice(2));
331
+ const starter = await resolveStarter({
332
+ flag: starterFlag,
333
+ isTTY: process.stdin.isTTY === true
334
+ });
335
+ const { adminEmail, adminPassword } = await writeProject({
336
+ targetDir,
337
+ starter,
338
+ imageTag,
339
+ force,
340
+ startersSourceDir: resolveStartersSourceDir()
341
+ });
342
+ process.stdout.write(`
343
+ Scaffolded boject-cms project at ${targetDir}
344
+
345
+ Next steps:
346
+ cd ${targetDir}
347
+ docker compose up -d
348
+
349
+ Once the container is healthy, log in at http://localhost:4000/login with:
350
+ Email: ${adminEmail}
351
+ Password: ${adminPassword}
352
+
353
+ This password is also saved in .env \u2014 you will NOT see it again.
354
+ `);
355
+ }
356
+ main().catch((error) => {
357
+ const message = error instanceof Error ? error.message : String(error);
358
+ process.stderr.write(`Error: ${message}
359
+ `);
360
+ process.exit(1);
361
+ });