@elytracms/cli 0.0.92 → 0.0.93

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/cli",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.92",
4
+ "version": "0.0.93",
5
5
  "description": "The Elytra CLI — run a consuming repo's studio, frontend, and Convex backend under one command.",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "node": ">=22.18.0"
18
18
  },
19
19
  "dependencies": {
20
- "@elytracms/core": "^0.0.92"
20
+ "@elytracms/core": "^0.0.93"
21
21
  },
22
22
  "devDependencies": {
23
23
  "vitest": "^4.1.10"
package/src/cli.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  devFrontendUrl,
7
7
  } from '@elytracms/core/export-sync/deployment-ready'
8
8
  import { resolveInstalledEngineVersion, runDoctor } from './doctor.js'
9
+ import { formatDoctorChecksHelp } from './doctor-checks.js'
9
10
  import { appDisplay } from './layout.js'
10
11
  import { runPreviews } from './previews.js'
11
12
  import { inspectProject, resolveStudioEnv } from './project.js'
@@ -29,16 +30,10 @@ Commands:
29
30
  elytra build frontend frontend build only
30
31
  init First-run setup: provision Convex, generate the auth signing keys,
31
32
  verify readiness. Idempotent — safe to re-run at any point.
32
- doctor Diagnose the active deployment's readiness (URL, reachability, auth
33
- signing keys, expected deployment env vars — set/missing by NAME,
34
- values never shown — config, membership) and print each fix. When the
35
- instant-publish revalidate pair is set, also sends a signed no-op ping
36
- to the endpoint to prove the publish → live chain end to end. Also
37
- checks your routes for URL conflicts, the project layout
38
- (package.json#elytra), the AI route your studio's vite.config.ts
39
- mounts, and reports the generated-previews artifacts (manifest/files
40
- present; hash staleness lives in \`elytra previews --check\`). Exit
41
- non-zero if any check fails. Read-only. Optional: --url <convex url>.
33
+ doctor Diagnose the deployment, its env, your config, routes, project layout
34
+ and generated previews, and print each fix. 'elytra doctor --help'
35
+ lists every check. Exit non-zero if any check fails. Read-only.
36
+ Optional: --url <convex url>.
42
37
  previews Render every canvas-eligible component's fixtures against YOUR theme
43
38
  (headless Chromium via Playwright — an optional install) and write
44
39
  committed preview images (studio/public/elytra-previews/) plus an
@@ -117,27 +112,23 @@ First-run setup: provision Convex, generate the auth signing keys, verify
117
112
  readiness. Idempotent — safe to re-run at any point. No flags.
118
113
  `.trim(),
119
114
 
115
+ // EC-599: the check list is rendered from the one declaration (`doctor-checks.js`) —
116
+ // this text used to restate it by hand, and had drifted from what the doctor does.
120
117
  doctor: `
121
118
  Usage: elytra doctor [--url <convex url>]
122
119
 
123
- Diagnose the active deployment's readiness (URL, reachability, auth signing
124
- keys, expected deployment env vars — set/missing by NAME, values never shown
125
- — config, membership) and print each fix. When the instant-publish
126
- revalidate pair is set, also sends a signed no-op ping to the endpoint to
127
- prove the publish → live chain end to end. Also checks your route set for
128
- URL conflicts (routes are code — redirect loops are editorial data and are
129
- checked in the studio), and reports the generated-previews artifacts
130
- (manifest/files present; hash staleness lives in \`elytra previews --check\`).
131
- With studio/ deps installed, config validity is checked for real: doctor
132
- executes elytra.config.ts through your studio's own Vite (takes a few
133
- seconds), and the route checks judge the routes it resolved; without deps
134
- it stays an honest "unknown" with the reason and the routes are read from
135
- cms/index.ts. With the config resolved, it also reads the public delivery
136
- snapshot and warns about published documents whose collection the config
137
- does not declare. It reports the project layout it resolved
138
- (package.json#elytra — a declared path that does not exist fails) and the AI
139
- route your studio's vite.config.ts mounts (which decides whether an AI
140
- provider key is expected). Exit non-zero if any check fails. Read-only.
120
+ Diagnose the active deployment and this project, and print a copy-pasteable
121
+ fix for every failing or warning check. Read-only: it reads the studio env,
122
+ the deployment's public endpoints and the names of the deployment's env vars;
123
+ no value is ever printed (the revalidate pair's are read in-process, only to
124
+ sign the probe ping). With the studio's dependencies installed it also
125
+ executes elytra.config.ts through your studio's own Vite (a few seconds), so
126
+ the config, collection and route checks get real answers; without them those
127
+ checks say "unknown" with the reason. Exit non-zero if any check fails —
128
+ warnings and unknowns never change the exit code.
129
+
130
+ Checks, in report order:
131
+ ${formatDoctorChecksHelp()}
141
132
 
142
133
  Options:
143
134
  --url <convex url> Diagnose this deployment instead of the one resolved
@@ -0,0 +1,261 @@
1
+ import { expectedDeploymentEnv } from '@elytracms/core/export-sync/deployment-ready'
2
+
3
+ /**
4
+ * Every check `elytra doctor` can print, declared ONCE (EC-599), in report order.
5
+ *
6
+ * The doctor's capabilities used to be written down three times by hand — the guide's
7
+ * table, `elytra --help` and `elytra doctor --help` — and two of them drifted: the help
8
+ * text still said redirect loops are "checked in the studio" and that the routes are read
9
+ * from `cms/index.ts`, long after EC-590 moved both. Now there is one list and three
10
+ * readers: `elytra doctor --help` ({@link formatDoctorChecksHelp}), the guide's check
11
+ * table (`docs/guide/06-cli-reference.md`, a region `pnpm check:guide` regenerates and
12
+ * fails on when stale — {@link formatDoctorChecksMarkdown}), and the report's bucket
13
+ * headings ({@link DOCTOR_BUCKET_TITLES}). `test/doctor-checks.test.js` runs the doctor
14
+ * and holds the ids it prints equal to this list, both directions, bucket by bucket — a
15
+ * check added without an entry, or an entry left behind by a removed check, fails there.
16
+ *
17
+ * Each entry: `id` (as printed; `env:*` stands for the one row per expected variable),
18
+ * `bucket`, `summary` (what it looks at — one sentence, printed by `--help`), and
19
+ * `outcome` (when it fails / warns / stays unknown — the guide prints it too).
20
+ */
21
+
22
+ /** The report's buckets, in report order, with the heading each prints under. */
23
+ export const DOCTOR_BUCKETS = [
24
+ { bucket: 'deployment', title: 'Deployment' },
25
+ { bucket: 'deployment-env', title: 'Deployment env' },
26
+ { bucket: 'app', title: 'App' },
27
+ { bucket: 'user', title: 'Per-user' },
28
+ { bucket: 'previews', title: 'Previews' },
29
+ ]
30
+
31
+ /** Bucket id → its heading in the printed report. */
32
+ export const DOCTOR_BUCKET_TITLES = Object.fromEntries(DOCTOR_BUCKETS.map(({ bucket, title }) => [bucket, title]))
33
+
34
+ export const DOCTOR_CHECKS = [
35
+ // --- deployment -----------------------------------------------------------------
36
+ {
37
+ id: 'backend-url',
38
+ bucket: 'deployment',
39
+ summary:
40
+ 'A backend is configured: `VITE_CONVEX_URL` (or `CONVEX_URL`) in the studio env, read by the parser the studio runs, or `--url`.',
41
+ outcome:
42
+ 'Fails with neither, and on a `VITE_ELYTRA_MODE` other than `playground`. A configured URL is diagnosed even when the studio itself runs playground.',
43
+ },
44
+ {
45
+ id: 'backend-reachable',
46
+ bucket: 'deployment',
47
+ summary: 'The deployment answers its query API as a Convex deployment — not merely any HTTP response.',
48
+ outcome: 'Fails when it does not (a nonexistent deployment’s empty 404 included).',
49
+ },
50
+ {
51
+ id: 'auth-signing-keys',
52
+ bucket: 'deployment',
53
+ summary:
54
+ 'The deployment’s public JWKS endpoint serves a signing key set — definitive from node (the browser cannot read it: the endpoint sets no CORS).',
55
+ outcome: 'Fails when the deployment publishes no key set (`JWKS` / `JWT_PRIVATE_KEY` unset) — every sign-in fails.',
56
+ },
57
+ {
58
+ id: 'deployment-provisioned',
59
+ bucket: 'deployment',
60
+ summary: 'A deployment is wired for the active project.',
61
+ outcome: 'Passes whenever a backend URL is configured (a URL is a wired deployment); `unknown` with none.',
62
+ },
63
+ {
64
+ id: 'functions-version',
65
+ bucket: 'deployment',
66
+ summary:
67
+ 'The engine version the deployed functions were stamped with, against the `@elytracms/studio` your studio has installed.',
68
+ outcome:
69
+ 'Fails when they differ or the stamp is missing; `unknown` when the installed studio version cannot be read here.',
70
+ },
71
+ // --- deployment env -------------------------------------------------------------
72
+ {
73
+ id: 'env:*',
74
+ bucket: 'deployment-env',
75
+ summary:
76
+ 'One row per expected variable, by name only — the deployment’s (`npx convex env list` on the linked deployment) and, when the studio mounts an AI route, the studio host’s provider key. No value reaches the report.',
77
+ outcome:
78
+ 'A required variable unset fails, a recommended one warns; a half-set group (one of the revalidate pair) fails. `unknown` with no linked deployment, or under `--url`.',
79
+ },
80
+ {
81
+ id: 'revalidate-probe',
82
+ bucket: 'deployment-env',
83
+ summary:
84
+ 'When the revalidate pair is set: a signed no-op `probe` event POSTed to the endpoint, proving the publish → live chain end to end.',
85
+ outcome:
86
+ 'Fails on any answer but the route’s `200 {"revalidated":[]}` — a secret mismatch or an unset frontend secret (401), a frontend on another Elytra version (400), no route (404). Warns when the endpoint cannot be reached from this machine.',
87
+ },
88
+ // --- app ------------------------------------------------------------------------
89
+ {
90
+ id: 'config-valid',
91
+ bucket: 'app',
92
+ summary:
93
+ '`elytra.config.ts` executed through your studio’s own Vite (needs the studio’s dependencies installed) and run through the one resolver the studio boots through.',
94
+ outcome:
95
+ 'Fails with every located issue (`projects.0.slug: …`); `unknown`, with the reason, when it cannot be executed here. The checks below that read the config are `unknown` then too.',
96
+ },
97
+ {
98
+ id: 'asset-collection',
99
+ bucket: 'app',
100
+ summary: 'A project whose collections declare asset fields resolves an asset collection.',
101
+ outcome: 'Fails when none resolves.',
102
+ },
103
+ {
104
+ id: 'presentation-targets',
105
+ bucket: 'app',
106
+ summary: 'Every collection’s `presentation.body` names fields the collection declares.',
107
+ outcome: 'Fails on a name the collection does not have.',
108
+ },
109
+ {
110
+ id: 'published-orphans',
111
+ bucket: 'app',
112
+ summary:
113
+ 'Published documents of collections the config does not declare, read from the deployment’s public delivery snapshot.',
114
+ outcome: 'Warns, naming each collection and count. Printed only against a deployment, with the config resolved.',
115
+ },
116
+ {
117
+ id: 'project-layout',
118
+ bucket: 'app',
119
+ summary:
120
+ 'The studio, frontend and theme locations the CLI resolved from `package.json#elytra`, and whether each was declared or defaulted.',
121
+ outcome: 'Fails on an invalid section or a declared path that does not exist.',
122
+ },
123
+ {
124
+ id: 'ai-route',
125
+ bucket: 'app',
126
+ summary:
127
+ 'The AI route your studio’s `vite.config.ts` mounts (`createAiDevPlugin`, read from the loaded config) — it decides whether an AI provider key is expected.',
128
+ outcome: 'Fails when the mounted `handlerModulePath` does not exist.',
129
+ },
130
+ {
131
+ id: 'route-conflicts',
132
+ bucket: 'app',
133
+ summary:
134
+ 'Two routes tied on specificity, locale scope and shape — the one router’s issue set (the same set the studio and the delivery host read), over the routes your config resolved.',
135
+ outcome: 'Fails on a tie; `unknown` when the config could not be resolved here (as are the next three).',
136
+ },
137
+ {
138
+ id: 'mount-conflicts',
139
+ bucket: 'app',
140
+ summary: 'Two hierarchy mounts on the same path in the same locale scope.',
141
+ outcome: 'Fails on a tie.',
142
+ },
143
+ {
144
+ id: 'unreachable-routes',
145
+ bucket: 'app',
146
+ summary:
147
+ 'A declaration that can never serve content — an all-dynamic route such as `/:slug` shadowing a mount’s whole depth, a mount on a collection with no parent role, a catch-all with a dynamic prefix like `/:lang/:path*`.',
148
+ outcome: 'Fails on such a declaration; warns on a precedence that depends on content.',
149
+ },
150
+ {
151
+ id: 'redirect-loops',
152
+ bucket: 'app',
153
+ summary:
154
+ 'The deployment’s real redirects — the enabled ones its public delivery snapshot serves, read on the same request as `published-orphans`.',
155
+ outcome: 'Fails on a loop or a duplicate `from` (`/old` = `/old/`); `unknown` when no deployment was read.',
156
+ },
157
+ // --- per-user -------------------------------------------------------------------
158
+ {
159
+ id: 'authenticated',
160
+ bucket: 'user',
161
+ summary: 'The signed-in session.',
162
+ outcome: 'Always `unknown` here — the studio boot checks it.',
163
+ },
164
+ {
165
+ id: 'member-role',
166
+ bucket: 'user',
167
+ summary: 'The signed-in user’s membership of the project.',
168
+ outcome: 'Always `unknown` here — the studio boot checks it.',
169
+ },
170
+ // --- previews -------------------------------------------------------------------
171
+ {
172
+ id: 'previews-artifacts',
173
+ bucket: 'previews',
174
+ summary: 'Printed alone when the studio holds no generated previews at all (they are optional).',
175
+ outcome: 'Always ok.',
176
+ },
177
+ {
178
+ id: 'previews-manifest',
179
+ bucket: 'previews',
180
+ summary: 'The generated previews manifest (`<studio>/elytra-previews.json`) is present and parses.',
181
+ outcome: 'Warns otherwise.',
182
+ },
183
+ {
184
+ id: 'previews-files',
185
+ bucket: 'previews',
186
+ summary: 'Every manifest entry’s image file is present.',
187
+ outcome: 'Warns, naming the first missing file.',
188
+ },
189
+ {
190
+ id: 'previews-orphans',
191
+ bucket: 'previews',
192
+ summary: 'Image files no manifest entry references.',
193
+ outcome: 'Warns, naming the first.',
194
+ },
195
+ {
196
+ id: 'previews-staleness',
197
+ bucket: 'previews',
198
+ summary: 'Input-hash staleness needs the component registry, so it is not judged here.',
199
+ outcome: 'Always `unknown` — run `elytra previews --check`.',
200
+ },
201
+ ]
202
+
203
+ /** Is a printed check id the catalogue entry `entry`? (`env:*` covers every `env:<NAME>` row.) */
204
+ export function isCheckOf(entry, id) {
205
+ return entry.id.endsWith(':*') ? id.startsWith(entry.id.slice(0, -1)) : entry.id === id
206
+ }
207
+
208
+ /** The expected deployment variables, read from core's one declaration (every one the doctor may list). */
209
+ function expectedEnvNames() {
210
+ return expectedDeploymentEnv({ aiRouteConfigured: true }).map((expected) =>
211
+ expected.anyOf ? expected.anyOf.join(' / ') : expected.name,
212
+ )
213
+ }
214
+
215
+ /** Wrap prose to `width` columns, every line prefixed with `indent`. */
216
+ function wrap(text, indent, width = 80) {
217
+ const lines = []
218
+ let line = ''
219
+ for (const word of text.split(/\s+/)) {
220
+ if (line && indent.length + line.length + 1 + word.length > width) {
221
+ lines.push(indent + line)
222
+ line = word
223
+ } else {
224
+ line = line ? `${line} ${word}` : word
225
+ }
226
+ }
227
+ if (line) lines.push(indent + line)
228
+ return lines.join('\n')
229
+ }
230
+
231
+ /** The check list `elytra doctor --help` prints, grouped by bucket. Pure. */
232
+ export function formatDoctorChecksHelp() {
233
+ const out = []
234
+ for (const { bucket, title } of DOCTOR_BUCKETS) {
235
+ out.push(` ${title}:`)
236
+ for (const check of DOCTOR_CHECKS.filter((entry) => entry.bucket === bucket)) {
237
+ out.push(` ${check.id}`)
238
+ const summary =
239
+ check.id === 'env:*' ? `${check.summary} Expected: ${expectedEnvNames().join(', ')}.` : check.summary
240
+ out.push(wrap(summary.replace(/`/g, ''), ' '))
241
+ }
242
+ }
243
+ return out.join('\n')
244
+ }
245
+
246
+ /** The guide's check table (`docs/guide/06-cli-reference.md`, a generated region). Pure. */
247
+ export function formatDoctorChecksMarkdown() {
248
+ const out = ['| Bucket | Check | What it looks at | Outcome |', '| --- | --- | --- | --- |']
249
+ for (const { bucket, title } of DOCTOR_BUCKETS) {
250
+ DOCTOR_CHECKS.filter((entry) => entry.bucket === bucket).forEach((check, index) => {
251
+ const summary =
252
+ check.id === 'env:*'
253
+ ? `${check.summary} Expected: ${expectedEnvNames()
254
+ .map((name) => `\`${name}\``)
255
+ .join(', ')}.`
256
+ : check.summary
257
+ out.push(`| ${index === 0 ? `**${title}**` : ''} | \`${check.id}\` | ${summary} | ${check.outcome} |`)
258
+ })
259
+ }
260
+ return out.join('\n')
261
+ }
package/src/doctor.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  probeRevalidateEndpoint,
14
14
  } from '@elytracms/core/export-sync/deployment-ready'
15
15
  import { aiRouteDoctorChecks, executeWorkspaceConfig, readStudioAiRoute } from './config-harness.js'
16
+ import { DOCTOR_BUCKET_TITLES } from './doctor-checks.js'
16
17
  import { appDisplay, layoutDoctorChecks } from './layout.js'
17
18
  import { convexEnvGet, defaultConvexExec } from './provision.js'
18
19
  import { gatherPreviewsArtifacts, previewsDoctorChecks } from './previews.js'
@@ -403,13 +404,8 @@ export async function gatherRevalidateProbe({
403
404
  // --- Formatting (pure) -------------------------------------------------------
404
405
 
405
406
  const GLYPH = { ok: '✓', warn: '!', fail: '✗', unknown: '?' }
406
- const BUCKET_TITLE = {
407
- deployment: 'Deployment',
408
- 'deployment-env': 'Deployment env',
409
- app: 'App',
410
- user: 'Per-user',
411
- previews: 'Previews',
412
- }
407
+ // The bucket headings are declared once, with the checks (`doctor-checks.js`, EC-599).
408
+ const BUCKET_TITLE = DOCTOR_BUCKET_TITLES
413
409
 
414
410
  /**
415
411
  * Splice extra checks into an existing bucket's run, right after that bucket's last