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 +95 -0
- package/README.md +127 -0
- package/dist/index.js +361 -0
- package/dist/starters/base.boject.json +502 -0
- package/dist/starters/rugby.boject.json +851 -0
- package/dist/starters/sport.boject.json +804 -0
- package/package.json +54 -0
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
|
+
[](https://www.npmjs.com/package/create-boject-cms)
|
|
4
|
+
[](./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
|
+
});
|