@elytracms/cli 0.0.85 → 0.0.86
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 +17 -3
- package/src/doctor.js +69 -12
- package/src/previews.js +3 -1
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.86",
|
|
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.86"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"vitest": "^4.1.10"
|
package/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { stdout } from 'node:process'
|
|
4
|
-
import { runDoctor } from './doctor.js'
|
|
4
|
+
import { resolveInstalledEngineVersion, runDoctor } from './doctor.js'
|
|
5
5
|
import { runPreviews } from './previews.js'
|
|
6
6
|
import { inspectProject, resolveStudioEnv } from './project.js'
|
|
7
7
|
import { ensureAuthKeys } from './provision.js'
|
|
@@ -426,7 +426,17 @@ export async function runInit(project, { log: emit = log, fetchImpl, exec, provi
|
|
|
426
426
|
emit('')
|
|
427
427
|
emit('3. Verify —')
|
|
428
428
|
const env = resolveStudioEnv(project.root)
|
|
429
|
-
|
|
429
|
+
// EC-552: with the studio dir known, the verify step includes the version
|
|
430
|
+
// handshake — right after a provision is exactly when a stale-functions state
|
|
431
|
+
// should be loud. Resolution happens HERE (the entry), keeping runDoctor injected.
|
|
432
|
+
const verifyStudioDir = project.apps.studio ? join(project.root, 'studio') : undefined
|
|
433
|
+
const code = await runDoctor({
|
|
434
|
+
env,
|
|
435
|
+
fetchImpl,
|
|
436
|
+
log: (l) => emit(` ${l}`),
|
|
437
|
+
studioDir: verifyStudioDir,
|
|
438
|
+
installedEngineVersion: resolveInstalledEngineVersion(verifyStudioDir),
|
|
439
|
+
})
|
|
430
440
|
|
|
431
441
|
// 4. Ready checklist.
|
|
432
442
|
emit('')
|
|
@@ -470,6 +480,7 @@ export async function run(argv, { cwd = process.cwd(), env = process.env, emit =
|
|
|
470
480
|
return 1
|
|
471
481
|
}
|
|
472
482
|
const studioEnv = resolveStudioEnv(project.root, env)
|
|
483
|
+
const doctorStudioDir = project.apps.studio ? join(project.root, 'studio') : undefined
|
|
473
484
|
return runDoctor({
|
|
474
485
|
env: studioEnv,
|
|
475
486
|
fetchImpl,
|
|
@@ -477,7 +488,10 @@ export async function run(argv, { cwd = process.cwd(), env = process.env, emit =
|
|
|
477
488
|
url: flagValue(argv, '--url'),
|
|
478
489
|
// EC-399: where `convex env list` runs (name enumeration — presence only),
|
|
479
490
|
// and whether the AI provider-key expectation applies.
|
|
480
|
-
studioDir:
|
|
491
|
+
studioDir: doctorStudioDir,
|
|
492
|
+
// EC-552: the installed engine version — resolved at the entry so runDoctor
|
|
493
|
+
// stays pure/injected; unresolvable (deps not installed) → honest `unknown`.
|
|
494
|
+
installedEngineVersion: resolveInstalledEngineVersion(doctorStudioDir),
|
|
481
495
|
// EC-469: where the project's `cms/` route set is read from (code, not backend).
|
|
482
496
|
root: project.root,
|
|
483
497
|
aiRouteConfigured: project.hasAiRoute,
|
package/src/doctor.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
1
4
|
import {
|
|
2
5
|
REVALIDATE_ENV_ENDPOINT,
|
|
3
6
|
REVALIDATE_ENV_SECRET,
|
|
@@ -5,6 +8,7 @@ import {
|
|
|
5
8
|
checkDeploymentReady,
|
|
6
9
|
expectedDeploymentEnv,
|
|
7
10
|
probeAuthSigningKeys,
|
|
11
|
+
probeDeployedFunctionsVersion,
|
|
8
12
|
probeRevalidateEndpoint,
|
|
9
13
|
} from '@elytracms/core/export-sync/deployment-ready'
|
|
10
14
|
import { executeWorkspaceConfig } from './config-harness.js'
|
|
@@ -68,16 +72,27 @@ export function resolveDoctorTarget(env = {}, { url } = {}) {
|
|
|
68
72
|
// --- Probes (node) -----------------------------------------------------------
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
75
|
+
* Deployment probe (EC-542 + EC-552): ONE call to the public query API yields BOTH the
|
|
76
|
+
* reachability fact and the version-handshake probe. The studio's probe was always a
|
|
77
|
+
* real query (`adapter.status()` → `members:mine`); the CLI previously used a bare GET
|
|
78
|
+
* and called ANY HTTP response "answered" — measured false-positive: Convex's wildcard
|
|
79
|
+
* `*.convex.cloud` 404s (empty body) for deployments that DO NOT EXIST, so every
|
|
80
|
+
* nonexistent deployment passed. Reachability now means "answered AS a Convex
|
|
81
|
+
* deployment": a Convex envelope — success, missing-function, or any other Convex
|
|
82
|
+
* error — proves a deployment; a non-Convex answer (the empty 404) or a thrown fetch
|
|
83
|
+
* (DNS/connection) fails it, each with its own honest detail so "nothing there" and
|
|
84
|
+
* "there but not deployed" stay distinguishable (the latter is reachable=true + the
|
|
85
|
+
* `functions-version`/JWKS-404 checks telling the deploy story).
|
|
74
86
|
*/
|
|
75
|
-
export async function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
export async function probeDeployment(url, fetchImpl = fetch) {
|
|
88
|
+
const versionProbe = await probeDeployedFunctionsVersion(url, fetchImpl)
|
|
89
|
+
const answered =
|
|
90
|
+
versionProbe.status === 'answered' ||
|
|
91
|
+
versionProbe.status === 'missing' ||
|
|
92
|
+
versionProbe.answeredAsConvex === true
|
|
93
|
+
return {
|
|
94
|
+
reachable: answered ? { available: true } : { available: false, detail: versionProbe.detail },
|
|
95
|
+
versionProbe,
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
98
|
|
|
@@ -86,24 +101,54 @@ export async function probeReachable(url, fetchImpl = fetch) {
|
|
|
86
101
|
* user facts are intentionally omitted (no auth session) → `unknown`; the app facts
|
|
87
102
|
* (config/collections) are gathered separately via the consumer-Vite harness when it
|
|
88
103
|
* is available (EC-495, {@link configFactsFromOutcome}).
|
|
104
|
+
*
|
|
105
|
+
* EC-542/EC-552: reachability and the version probe are ONE network call
|
|
106
|
+
* ({@link probeDeployment}); the `functions-version` fact is emitted only when the
|
|
107
|
+
* caller supplies `installedEngineVersion` (otherwise the probe outcome still decides
|
|
108
|
+
* reachability but the handshake stays an honest `unknown`).
|
|
89
109
|
*/
|
|
90
|
-
export async function gatherFacts(target, { fetchImpl = fetch } = {}) {
|
|
110
|
+
export async function gatherFacts(target, { fetchImpl = fetch, installedEngineVersion } = {}) {
|
|
91
111
|
if (target.kind === 'playground') return { backend: { kind: 'playground' } }
|
|
92
112
|
if (target.kind === 'unconfigured') return { backend: { kind: 'unconfigured', detail: target.detail } }
|
|
93
113
|
|
|
94
|
-
const reachable = await
|
|
114
|
+
const { reachable, versionProbe } = await probeDeployment(target.url, fetchImpl)
|
|
95
115
|
const siteUrl = backendSiteUrl(target.url, target.siteUrl)
|
|
96
116
|
const authKeys = await probeAuthSigningKeys(siteUrl, fetchImpl)
|
|
97
117
|
return {
|
|
98
118
|
backend: { kind: 'convex', url: target.url },
|
|
99
119
|
reachable,
|
|
100
120
|
authKeys,
|
|
121
|
+
...(installedEngineVersion
|
|
122
|
+
? { functionsVersion: { probe: versionProbe, expected: installedEngineVersion } }
|
|
123
|
+
: {}),
|
|
101
124
|
// A configured URL means a deployment is wired; leave `provisioned` unset (→ ok).
|
|
102
125
|
// session omitted → the user checks surface as `unknown`; config/collections are
|
|
103
126
|
// merged in by the caller from the EC-495 harness outcome (or stay omitted).
|
|
104
127
|
}
|
|
105
128
|
}
|
|
106
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the engine version of the `@elytracms/studio` package INSTALLED in the
|
|
132
|
+
* consumer's studio app (EC-552) — the `expected` half of the version handshake.
|
|
133
|
+
* Deliberately resolved through the `convex/delivery` export and its adjacent
|
|
134
|
+
* `package.json`: that is the very source file the consumer's `convex deploy`
|
|
135
|
+
* bundles, so the version read here is by construction the version a redeploy
|
|
136
|
+
* would stamp. (The consumer's own `package.json` only carries a RANGE.)
|
|
137
|
+
* `undefined` when the deps aren't installed → the fact is omitted → the check
|
|
138
|
+
* reports an honest `unknown`, matching the config checks' opportunistic contract.
|
|
139
|
+
*/
|
|
140
|
+
export function resolveInstalledEngineVersion(studioDir) {
|
|
141
|
+
if (!studioDir) return undefined
|
|
142
|
+
try {
|
|
143
|
+
const req = createRequire(join(studioDir, 'package.json'))
|
|
144
|
+
const deliverySource = req.resolve('@elytracms/studio/convex/delivery')
|
|
145
|
+
const pkg = JSON.parse(readFileSync(join(dirname(deliverySource), '..', 'package.json'), 'utf8'))
|
|
146
|
+
return typeof pkg.version === 'string' ? pkg.version : undefined
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
107
152
|
// --- Config facts via the consumer-Vite harness (EC-495) -------------------------------
|
|
108
153
|
|
|
109
154
|
/**
|
|
@@ -389,6 +434,13 @@ export function formatReport(report) {
|
|
|
389
434
|
* works-without-node_modules contract is untouched. A resolved config also hands the
|
|
390
435
|
* route checks the same collections the studio actually mounts (superseding the
|
|
391
436
|
* `cms/index.ts` re-export). `configOutcome` is injectable for tests.
|
|
437
|
+
*
|
|
438
|
+
* EC-552: when the caller supplies `installedEngineVersion` (the CLI entry resolves it
|
|
439
|
+
* from the consumer's installed `@elytracms/studio` via {@link
|
|
440
|
+
* resolveInstalledEngineVersion}), the deployed functions are probed for their stamped
|
|
441
|
+
* version and the `functions-version` check gets a real answer; otherwise the fact is
|
|
442
|
+
* omitted → an honest `unknown`. Resolution stays at the entry point so this function
|
|
443
|
+
* remains pure/injected like every other gatherer here.
|
|
392
444
|
*/
|
|
393
445
|
export async function runDoctor({
|
|
394
446
|
env = {},
|
|
@@ -402,6 +454,7 @@ export async function runDoctor({
|
|
|
402
454
|
previewsArtifacts,
|
|
403
455
|
routeFacts,
|
|
404
456
|
configOutcome,
|
|
457
|
+
installedEngineVersion,
|
|
405
458
|
} = {}) {
|
|
406
459
|
const target = resolveDoctorTarget(env, { url })
|
|
407
460
|
|
|
@@ -417,7 +470,11 @@ export async function runDoctor({
|
|
|
417
470
|
log('No backend configured.')
|
|
418
471
|
}
|
|
419
472
|
|
|
420
|
-
|
|
473
|
+
// EC-552: the version handshake rides the same probe as reachability; it becomes a
|
|
474
|
+
// real check only when the caller supplied the installed engine version (the CLI
|
|
475
|
+
// entry resolves it via `resolveInstalledEngineVersion`; runDoctor itself never
|
|
476
|
+
// touches the filesystem for it, staying pure/injected like every gatherer here).
|
|
477
|
+
const facts = await gatherFacts(target, { fetchImpl, installedEngineVersion })
|
|
421
478
|
if (target.kind === 'convex') {
|
|
422
479
|
facts.deploymentEnv = gatherDeploymentEnvFacts({ env, target, studioDir, aiRouteConfigured, exec })
|
|
423
480
|
// EC-421: default-on signed no-op ping whenever the revalidate pair is present.
|
package/src/previews.js
CHANGED
|
@@ -51,7 +51,9 @@ export const PREVIEWS_MANIFEST_FILE = 'elytra-previews.json'
|
|
|
51
51
|
/** Root-relative URL prefix the manifest entries carry. */
|
|
52
52
|
export const PREVIEWS_URL_PREFIX = '/elytra-previews/'
|
|
53
53
|
/**
|
|
54
|
-
* Manifest format version (
|
|
54
|
+
* Manifest format version — THE declaration (EC-514: the studio's mirror was
|
|
55
|
+
* removed; its reader is version-agnostic, and this CLI is the only writer and the
|
|
56
|
+
* only reader of the number). Bump on incompatible schema/hash changes.
|
|
55
57
|
* `2` (EC-502): the entry hash no longer includes the engine version, so v1 hashes
|
|
56
58
|
* are not comparable with v2 ones — a version mismatch is reported as itself
|
|
57
59
|
* ("regenerate"), never as N bogus `stale` entries.
|