@boject/cli 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,352 @@
1
+ # @boject/cli
2
+
3
+ [![npm](https://img.shields.io/npm/v/@boject/cli)](https://www.npmjs.com/package/@boject/cli)
4
+ [![License: BUSL-1.1](https://img.shields.io/badge/license-BUSL--1.1-blue.svg)](./LICENSE)
5
+
6
+ Maintenance and schema-as-code CLI for [boject-cms](https://github.com/bojectify/boject-cms) projects.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pnpm add -D @boject/cli
12
+ # or globally
13
+ pnpm add -g @boject/cli
14
+ ```
15
+
16
+ The package ships a single `boject` binary.
17
+
18
+ ## Configuration
19
+
20
+ Most commands read a committed `.boject.config.json` at the project root and an API key from the `BOJECT_API_KEY` env var.
21
+
22
+ ```json
23
+ {
24
+ "cms": { "url": "https://cms.dev.example.com" },
25
+ "schema": { "path": "content-types/schema.boject.json" }
26
+ }
27
+ ```
28
+
29
+ The CLI walks up from the current working directory to find the config — same pattern as Prettier / ESLint / Vitest. A developer running `boject schema pull` from a subdirectory still picks up the project root's config.
30
+
31
+ The API key is per-user / per-environment and lives in `.env` (gitignored). Mint one in the CMS:
32
+
33
+ ```bash
34
+ # In the CMS repo
35
+ pnpm apikey:create my-cli --scopes schema:read,schema:write
36
+ ```
37
+
38
+ Then put the printed `boject_...` key in your shell or project `.env`:
39
+
40
+ ```bash
41
+ export BOJECT_API_KEY=boject_...
42
+ ```
43
+
44
+ ## Commands
45
+
46
+ ### `boject upgrade`
47
+
48
+ Upgrade the CMS image tag in the current directory's `docker-compose.yml`, pull the new image, restart the container, and wait for the health endpoint.
49
+
50
+ ```bash
51
+ boject upgrade # latest semver tag from the registry
52
+ boject upgrade --to 1.4.2 # specific version
53
+ boject upgrade --check # exit 1 if an upgrade is available, 0 otherwise
54
+ boject upgrade --dry-run # show the diff without applying
55
+ ```
56
+
57
+ ### `boject schema pull [--out <path>] [--url <url>]`
58
+
59
+ Fetches the live schema from the CMS and writes it to `content-types/schema.boject.json` (or `--out`). Always overwrites — review the diff with `git diff`.
60
+
61
+ ```bash
62
+ boject schema pull
63
+ # ✓ Pulled schema from https://cms.dev.example.com
64
+ # 4 content types, 23 fields
65
+ # Wrote /path/to/project/content-types/schema.boject.json (3247 bytes)
66
+ ```
67
+
68
+ ### `boject schema validate [<path>]`
69
+
70
+ Validates a bundle file's shape and runs the schema planner against an empty snapshot to surface cross-reference issues (e.g. a `RELATION` pointing at a content type that isn't declared in the same bundle). **No network — safe for pre-commit hooks.**
71
+
72
+ ```bash
73
+ boject schema validate
74
+ # ✓ Bundle valid
75
+ # 4 content types, 23 fields, 0 cross-reference issues
76
+
77
+ boject schema validate ./content-types/schema.boject.json
78
+ # ✗ Bundle invalid
79
+ # - field.type: Article.publishDate has invalid type "DATE"
80
+ # - relation: Article.author targets unknown content type "Auther"
81
+ ```
82
+
83
+ If `<path>` is omitted, falls back to the configured `schema.path`.
84
+
85
+ ### `boject schema apply [<path>] [--allow-destructive] [--dry-run]`
86
+
87
+ Pushes the local bundle to the CMS via `POST /api/schema/apply`. The 99% workflow is "edit in dev, export, commit, deploy" — the entrypoint applier picks up the committed file on the next boot, so `apply` is the escape hatch for headless / one-off scenarios where redeploying is too heavyweight.
88
+
89
+ ```bash
90
+ boject schema apply
91
+ # ✓ Applied
92
+ # 1 content type updated
93
+ # 2 fields created
94
+
95
+ boject schema apply --dry-run # server runs the apply but rolls back the transaction
96
+ # ✓ Dry run
97
+ # 1 content type created
98
+ # 1 field created
99
+
100
+ boject schema apply --allow-destructive # required to remove types/fields
101
+ ```
102
+
103
+ **Output on rejection:**
104
+
105
+ - `BUNDLE_INVALID` → list of validator errors, exit 1.
106
+ - `SCHEMA_APPLY_BLOCKED` → list of blocker codes/paths/messages, exit 1.
107
+ - `SCHEMA_CHANGED_DURING_APPLY` → auto-retried once; exit 1 if the second attempt also races.
108
+
109
+ **Required scope:** `schema:write`.
110
+
111
+ ### `boject schema check`
112
+
113
+ Pulls the live schema and diffs it against the on-disk bundle. Exits 0 if in sync, 1 if drift detected — designed for CI.
114
+
115
+ ```bash
116
+ boject schema check
117
+ # ✓ Schema in sync with https://cms.dev.example.com
118
+
119
+ boject schema check
120
+ # ✗ Drift detected against https://cms.dev.example.com
121
+ # - Article: field 'subtitle' exists locally but not on the server
122
+ # - Article: field 'tagline' exists on the server but not locally
123
+ # Run `boject schema pull` to update the local file.
124
+ ```
125
+
126
+ CI pattern:
127
+
128
+ ```yaml
129
+ - run: BOJECT_API_KEY=$BOJECT_API_KEY pnpm boject schema check
130
+ ```
131
+
132
+ ### `boject perf <command>`
133
+
134
+ Operator-facing load-test runner. Spawns [k6](https://k6.io/) against your CMS, emits a markdown + JSON report. Run read-only against an existing dataset, or seed first via SQL — see the subcommands below.
135
+
136
+ **Prerequisites:** [k6](https://k6.io/docs/get-started/installation/) on PATH (`brew install k6` on macOS). The CLI fails fast with an install hint if it isn't.
137
+
138
+ **Required scope:** `content:read`.
139
+
140
+ > **Local smoke tests:** when running against `http://localhost:...`, lower the rate with `--target-rps 200` (or similar). The default 50→2000 RPS ramp opens enough TCP connections to exhaust your OS's ephemeral port pool partway through (~30k ports on macOS, with `TIME_WAIT` holding sockets for 60–120s after close). You'll see `dial tcp: connect: can't assign requested address` once the pool runs dry. The default ramp is calibrated for perf-clones behind a load balancer, not single-machine localhost runs. The `boject perf sweep` defaults are equally heavy — same advice applies.
141
+
142
+ `.boject.config.json` may include an optional `perf` section to cache the common flags:
143
+
144
+ ```json
145
+ {
146
+ "cms": { "url": "https://cms-staging.example.com" },
147
+ "schema": { "path": "content-types/schema.boject.json" },
148
+ "perf": {
149
+ "contentType": "Article",
150
+ "filterField": "publishDate",
151
+ "relationField": "author",
152
+ "out": "./perf-reports"
153
+ }
154
+ }
155
+ ```
156
+
157
+ #### `boject perf check --content-type <id> [flags]`
158
+
159
+ Preflight: verifies k6 is on PATH, the target is reachable, the API key works, the content type exists, and a DATETIME / single-target RELATION field can be selected for the `filtered` / `relation` query shapes. Doesn't run any load.
160
+
161
+ ```bash
162
+ boject perf check --content-type Article
163
+ # Preflight OK ✓
164
+ # list field: articleList
165
+ # filter field: publishDate
166
+ # relation field: author
167
+ ```
168
+
169
+ Exits 0 on success, 2 on environment problems (k6 missing, target unreachable, key invalid), 3 on input problems (missing flag).
170
+
171
+ #### `boject perf scenario <name> --content-type <id> [flags]`
172
+
173
+ Run one scenario. `<name>` is `graphql-flat` or `graphql-sitemap`.
174
+
175
+ - **`graphql-flat`** — RPS ramp 50→2000 over 3 minutes against three query shapes (`bare` / `filtered` / `relation`). Heavy load — fires a TTY confirm prompt before starting; bypass with `--yes` for CI.
176
+ - **`graphql-sitemap`** — Cursor-paginated drain of the content type. Lighter; no confirm prompt.
177
+
178
+ ```bash
179
+ boject perf scenario graphql-sitemap --content-type Article
180
+ # [k6] running (10.4s), 100/100 VUs, 1 complete and 0 interrupted iterations
181
+ # Report written to perf-reports/2026-05-06T14-32-11Z-a3f9
182
+ ```
183
+
184
+ Power-user overrides for `graphql-flat`:
185
+
186
+ - `--target-rps <n>` — peak RPS. Default 2000. The 6-stage ramp scales proportionally.
187
+ - `--stages <csv>` — explicit RPS stages, e.g. `50,100,500,2000`. Overrides the scaled ramp wholesale.
188
+
189
+ Field overrides (introspection picks defaults):
190
+
191
+ - `--filter-field <id>` — DATETIME field for the `filtered` shape. Skipped if absent.
192
+ - `--relation-field <id>` — single-target RELATION field for the `relation` shape. Skipped if absent.
193
+
194
+ #### `boject perf seed --content-type <id> [flags]`
195
+
196
+ > ⚠ **Destructive — throwaway DBs only.**
197
+ > `boject perf reset` and `boject perf seed --reset` issue an unconditional `TRUNCATE` against the target DB's `ContentEntry` and `ContentEntryVersion` tables. **Every entry is wiped, not just the ones this CLI created.** Only run against a disposable `_perf` or `_staging` clone you can rebuild. The `--allow-database <name>` escape hatch exists for non-conforming clone names; never reach for it to point at a production or dev database.
198
+
199
+ Generates and writes deterministic seed entries for a content type. Uses `--database-url <perf>` — raw `pg` writes. Refuses any database whose name doesn't end in `_perf` / `_staging` unless `--allow-database` lists it. Pair with `--reset` for a clean target.
200
+
201
+ Use `--size <n>` to control the entry count and `--seed <n>` (default `1`) to pick the deterministic data set.
202
+
203
+ ##### Conflict handling on re-run
204
+
205
+ If the target DB already contains entries with titles or slugs that match what the seed step would generate (this happens when re-running with the default `--seed 1` against a populated DB), the seed step skips conflicting rows and continues. The final progress line surfaces how many were skipped:
206
+
207
+ ```
208
+ [perf:seed] inserted 950 / total 1000 (50 skipped) entries
209
+ ```
210
+
211
+ If the skip rate exceeds 50%, the seed step aborts with a `SeedMostlyDuplicateError` — this catches the case where the target DB is mostly already populated and the new run would do nothing useful. Two ways to unblock:
212
+
213
+ - **Pass `--seed <n>`** for a different deterministic data set.
214
+ - **Reset the target DB first:** `boject perf reset --database-url <url> --yes`.
215
+
216
+ **Cascade skips for cross-references.** When an entry is skipped (either via 409 conflict or because a previous entry it references was skipped), the skip propagates: any entry whose data points at the skipped entry's synthetic ID is also skipped. This prevents 400 errors from the CMS validating broken RELATION/MULTIRELATION/RICHTEXT references.
217
+
218
+ The threshold check (50%) accounts for both kinds of skip. A repeat seed against a populated DB with multi-group bundles (e.g. Author + Article) trips the threshold cleanly: authors collide, articles cascade-skip, everything is loudly reported, the operator passes `--seed <n>` or resets.
219
+
220
+ Cascade-skipped entries are filtered BEFORE the envelope `INSERT` — no orphan `ContentEntry` rows are created.
221
+
222
+ For SQL bundles with deferred-edge patches: if a patch's target or `fieldUpdates` reference a skipped entry, the patch is suppressed with an `stderr` log (`[perf:seed] skipping patch — ...`). Patch skips do NOT count toward the entry-level threshold — they're an internal mechanism for cross-group circular refs, not an operator-visible metric.
223
+
224
+ #### `boject perf scenario rest-crud-cycle --content-type <id> [flags]`
225
+
226
+ Write-load scenario: 10 VUs run interleaved CREATE / READ / DELETE iterations against `/api/entries`. Requires `--database-url <perf>`. Refuses `--read-only`. Use `--crud-n <n>` to control iterations per phase (default 10000, matches the canonical sweep).
227
+
228
+ ```bash
229
+ boject perf scenario rest-crud-cycle --content-type Article \
230
+ --database-url postgresql://boject:boject@localhost:5432/boject_perf \
231
+ --crud-n 50 --yes
232
+ ```
233
+
234
+ The report's `## Scenario 2 — REST CRUD cycle` section breaks down latency by phase (`create` / `read` / `list` / `delete`).
235
+
236
+ `rest-crud-cycle` does not run a seed step — the scenario's CREATE phase produces its own entries. Pass `--reset` if you want a clean DB before the run.
237
+
238
+ **Required scope:** `content:write`.
239
+
240
+ #### `boject perf sweep --content-type <id> [flags]`
241
+
242
+ Run both scenarios across a matrix. `graphql-sitemap` iterates `pageSizes × vusList`; `graphql-flat` runs all three query shapes once. Single combined report at the end.
243
+
244
+ ```bash
245
+ boject perf sweep --content-type Article --yes
246
+ # ... 12 k6 runs ...
247
+ # Sweep report written to perf-reports/2026-05-06T14-35-22Z-b1cd
248
+ ```
249
+
250
+ Matrix overrides:
251
+
252
+ - `--page-sizes <csv>` — default `100,500,1000`.
253
+ - `--vus <csv>` — default `1,5,20`.
254
+
255
+ Plus all the `boject perf scenario` flags (target RPS, stages, field overrides, etc.).
256
+
257
+ #### `boject perf report [--from <dir>] [--out <dir>]`
258
+
259
+ Re-render `summary.md`, `metadata.json`, and `metrics.csv` from an existing run. Useful when iterating on the renderer or after a partial run.
260
+
261
+ ```bash
262
+ boject perf report # re-render the latest run in ./perf-reports
263
+ boject perf report --from <dir> # re-render a specific run dir
264
+ ```
265
+
266
+ #### Output files
267
+
268
+ Every run writes to `<out>/<timestamp>/`:
269
+
270
+ - `summary.md` — human-readable report (banners, scenario tables, run notes)
271
+ - `metadata.json` — machine-readable run context: target host, content type, fields used, intensity (RPS / stages), scenarios + outcomes, partial flag, schemaVersion. Consumed by downstream tooling. **API keys are never written.**
272
+ - `metrics.csv` — one row per `(scenario, page_size, shape)` with count, p50, p95, p99, error rate as a fraction.
273
+
274
+ **CSV column convention for `rest-crud-cycle`:** when the scenario is `rest-crud-cycle`, the `shape` column carries the phase value (`create` / `read` / `list` / `delete`) and the `page_size` column is `-`. This preserves the existing CSV schema without a `schemaVersion` bump — consumers parsing crud rows should know to read `shape` as the phase dimension.
275
+
276
+ - `raw.json` — k6 NDJSON output, one point per line.
277
+ - `k6-stderr.log` — sanitised stderr from k6 (no API keys).
278
+ - `pg-samples.csv` — **seed-direct only.** When `--database-url` is set, `boject perf scenario` / `sweep` runs a `pg_stat_activity` sampler alongside k6 (5s interval by default; override via `PERF_SAMPLER_INTERVAL_MS`). Peak and mean connection-pool counts are rendered into `summary.md` as a `Database connection pool` table. Operators using `--read-only` get no panel — there's no DB access in that mode.
279
+
280
+ #### Sanitisation
281
+
282
+ API keys are never logged to stdout/stderr or written to any output file. URLs with embedded credentials (`https://user:pass@host`) are stripped before logging. The k6 stderr log file is sanitised text (not a byte mirror) — defence in depth in case k6 ever echoes a Bearer header.
283
+
284
+ ## API key scopes
285
+
286
+ CMS API keys carry one or more scopes:
287
+
288
+ | Scope | Grants |
289
+ | -------------- | ----------------------------------------------------------------------------------- |
290
+ | `content:read` | Read content via the GraphQL endpoint (`/api/graphql`). Required for `boject perf`. |
291
+ | `schema:read` | Pull the schema bundle (`GET /api/schema/export`). |
292
+ | `schema:write` | Push schema (`POST /api/schema/apply`). |
293
+
294
+ A key created with `--scopes schema:read` cannot read content via GraphQL. A key created with `--scopes content:read,schema:read` can do both. Issue narrow keys for narrow automation.
295
+
296
+ ## Workflow
297
+
298
+ The intended development loop:
299
+
300
+ ```bash
301
+ # 1. Edit content types in the CMS UI in development.
302
+
303
+ # 2. Pull the live schema into your project.
304
+ boject schema pull
305
+
306
+ # 3. Review the diff and commit.
307
+ git diff content-types/schema.boject.json
308
+ git commit -am "Add 'publishedOn' field to Article"
309
+
310
+ # 4. Deploy. The container's entrypoint runs the applier on boot —
311
+ # no manual step required.
312
+ git push
313
+ ```
314
+
315
+ CI ensures committed schema matches the dev CMS:
316
+
317
+ ```bash
318
+ boject schema check
319
+ ```
320
+
321
+ ## Programmatic API
322
+
323
+ The four `runSchema*` functions are exported for embedding in scripts. They take a params object with `cwd`, `apiKey`, optional `flags`, and `stdout`/`stderr` callbacks; they return `{ exitCode: 0 | 1 }`.
324
+
325
+ ```ts
326
+ import { runSchemaPull } from '@boject/cli';
327
+
328
+ const result = await runSchemaPull({
329
+ cwd: process.cwd(),
330
+ apiKey: process.env.BOJECT_API_KEY,
331
+ stdout: (line) => console.log(line),
332
+ stderr: (line) => console.error(line),
333
+ });
334
+ process.exit(result.exitCode);
335
+ ```
336
+
337
+ ## Testing
338
+
339
+ Three Vitest projects, run via `pnpm test` (all) or individually:
340
+
341
+ - `pnpm test:unit` — hermetic unit tests. No external dependencies.
342
+ - `pnpm test:e2e` — end-to-end tests that build the CLI via `tsup` and exercise the built `dist/` binary. No DB.
343
+ - `pnpm test:integration` — exercises the SQL writer against a real Postgres. Requires `docker compose up -d` (or any Postgres on `localhost:5432` with the `boject` / `boject` user). Owns its own `boject_perf_test` database — created automatically on first run, schema-reset between suite runs. Does not touch `boject` (dev) or `boject_test` (cms integration).
344
+
345
+ ## See also
346
+
347
+ - The CMS itself: [`boject-cms`](https://github.com/bojectify/boject-cms)
348
+ - Project scaffolder: [`create-boject-cms`](https://www.npmjs.com/package/create-boject-cms)
349
+
350
+ ## License
351
+
352
+ 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.