@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 +2 -2
- package/src/cli.js +19 -28
- package/src/doctor-checks.js +261 -0
- package/src/doctor.js +3 -7
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.
|
|
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.
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|