@elytracms/cli 0.0.91 → 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 +36 -33
- package/src/config-harness.js +3 -2
- package/src/doctor-checks.js +261 -0
- package/src/doctor.js +51 -133
- package/src/engine-version.js +32 -0
- package/src/previews.js +4 -24
- package/src/project.js +2 -1
- package/src/provision.js +8 -6
- package/src/routes-check.js +86 -405
package/src/doctor.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { createRequire } from 'node:module'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
4
1
|
import { orphanedCollections } from '@elytracms/core/cms-core'
|
|
5
2
|
import {
|
|
6
3
|
REVALIDATE_ENV_ENDPOINT,
|
|
@@ -16,10 +13,11 @@ import {
|
|
|
16
13
|
probeRevalidateEndpoint,
|
|
17
14
|
} from '@elytracms/core/export-sync/deployment-ready'
|
|
18
15
|
import { aiRouteDoctorChecks, executeWorkspaceConfig, readStudioAiRoute } from './config-harness.js'
|
|
16
|
+
import { DOCTOR_BUCKET_TITLES } from './doctor-checks.js'
|
|
19
17
|
import { appDisplay, layoutDoctorChecks } from './layout.js'
|
|
20
18
|
import { convexEnvGet, defaultConvexExec } from './provision.js'
|
|
21
19
|
import { gatherPreviewsArtifacts, previewsDoctorChecks } from './previews.js'
|
|
22
|
-
import {
|
|
20
|
+
import { routesDoctorChecks } from './routes-check.js'
|
|
23
21
|
|
|
24
22
|
/**
|
|
25
23
|
* `elytra doctor` (EC-259) — the pre-boot counterpart to the studio's boot
|
|
@@ -145,26 +143,13 @@ export async function gatherFacts(target, { fetchImpl = fetch, installedEngineVe
|
|
|
145
143
|
}
|
|
146
144
|
|
|
147
145
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* `
|
|
152
|
-
*
|
|
153
|
-
* would stamp. (The consumer's own `package.json` only carries a RANGE.)
|
|
154
|
-
* `undefined` when the deps aren't installed → the fact is omitted → the check
|
|
155
|
-
* reports an honest `unknown`, matching the config checks' opportunistic contract.
|
|
146
|
+
* The engine version of the `@elytracms/studio` package INSTALLED in the consumer's
|
|
147
|
+
* studio app (EC-552) — the `expected` half of the version handshake. `undefined` when
|
|
148
|
+
* the deps aren't installed → the fact is omitted → the check reports an honest
|
|
149
|
+
* `unknown`. The one CLI resolver lives in `engine-version.js` (EC-583, RESIDUE-4e);
|
|
150
|
+
* re-exported here for the doctor's existing importers.
|
|
156
151
|
*/
|
|
157
|
-
export
|
|
158
|
-
if (!studioDir) return undefined
|
|
159
|
-
try {
|
|
160
|
-
const req = createRequire(join(studioDir, 'package.json'))
|
|
161
|
-
const deliverySource = req.resolve('@elytracms/studio/convex/delivery')
|
|
162
|
-
const pkg = JSON.parse(readFileSync(join(dirname(deliverySource), '..', 'package.json'), 'utf8'))
|
|
163
|
-
return typeof pkg.version === 'string' ? pkg.version : undefined
|
|
164
|
-
} catch {
|
|
165
|
-
return undefined
|
|
166
|
-
}
|
|
167
|
-
}
|
|
152
|
+
export { resolveInstalledEngineVersion } from './engine-version.js'
|
|
168
153
|
|
|
169
154
|
// --- Config facts via the consumer-Vite harness (EC-495) -------------------------------
|
|
170
155
|
|
|
@@ -419,13 +404,8 @@ export async function gatherRevalidateProbe({
|
|
|
419
404
|
// --- Formatting (pure) -------------------------------------------------------
|
|
420
405
|
|
|
421
406
|
const GLYPH = { ok: '✓', warn: '!', fail: '✗', unknown: '?' }
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
'deployment-env': 'Deployment env',
|
|
425
|
-
app: 'App',
|
|
426
|
-
user: 'Per-user',
|
|
427
|
-
previews: 'Previews',
|
|
428
|
-
}
|
|
407
|
+
// The bucket headings are declared once, with the checks (`doctor-checks.js`, EC-599).
|
|
408
|
+
const BUCKET_TITLE = DOCTOR_BUCKET_TITLES
|
|
429
409
|
|
|
430
410
|
/**
|
|
431
411
|
* Splice extra checks into an existing bucket's run, right after that bucket's last
|
|
@@ -475,93 +455,34 @@ export function formatReport(report) {
|
|
|
475
455
|
}
|
|
476
456
|
|
|
477
457
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
458
|
+
* What the doctor's route checks judge (EC-590; RESIDUE-4a/4b): the RESOLVED project's
|
|
459
|
+
* routes, collections and locales — the same records the studio mounts and delivery
|
|
460
|
+
* serves — and, when the deployment's public delivery snapshot was read, the enabled
|
|
461
|
+
* editorial redirects it serves. No resolved config → `unavailable`, with the reason:
|
|
462
|
+
* there is no second reading of the routes (the native `cms/index.ts` floor is gone).
|
|
463
|
+
* Pure; never throws.
|
|
484
464
|
*/
|
|
485
|
-
export
|
|
465
|
+
export function routeFactsFor({ outcome, configFacts = {}, snapshot }) {
|
|
486
466
|
const project = resolvedProjectOf(outcome)
|
|
487
|
-
if (project
|
|
488
|
-
|
|
467
|
+
if (!project || !Array.isArray(project.routes)) {
|
|
468
|
+
const reason =
|
|
469
|
+
configFacts.config !== undefined && !configFacts.config.valid
|
|
470
|
+
? 'fix the workspace config issues first (see config-valid).'
|
|
471
|
+
: (configFacts.configUnavailableDetail ?? 'elytra.config.ts was not evaluated here.')
|
|
472
|
+
return { unavailable: `Routes were not checked — ${reason}` }
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
project: {
|
|
489
476
|
routes: project.routes,
|
|
490
|
-
routeCount: project.routes.length,
|
|
491
477
|
...(Array.isArray(project.collections) ? { collections: project.collections } : {}),
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
const loaded = routeFacts ?? (await loadProjectRoutes(root))
|
|
496
|
-
const enriched = configFacts.collections ? { ...loaded, collections: configFacts.collections } : loaded
|
|
497
|
-
return { ...enriched, source: 'native' }
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// --- Frontend: `cacheComponents` (EC-515) --------------------------------------
|
|
501
|
-
|
|
502
|
-
/** The `next.config` file names Next itself resolves, in its order. */
|
|
503
|
-
export const NEXT_CONFIG_FILES = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.mts', 'next.config.cjs']
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* Read the frontend's `next.config` for the `cacheComponents` check — the file name and
|
|
507
|
-
* its text, or `{ configFile: null }` when the app has none. `undefined` when there is no
|
|
508
|
-
* frontend app to read (a studio-only repo: no row at all). Text only: the CLI runs
|
|
509
|
-
* without the consumer's `node_modules`, and a `next.config` imports Next-side modules.
|
|
510
|
-
* `readFile` is injectable for tests.
|
|
511
|
-
*/
|
|
512
|
-
export function gatherFrontendCacheComponents(frontendDir, readFile = (p) => readFileSync(p, 'utf8')) {
|
|
513
|
-
if (!frontendDir) return undefined
|
|
514
|
-
for (const name of NEXT_CONFIG_FILES) {
|
|
515
|
-
try {
|
|
516
|
-
return { configFile: name, text: readFile(join(frontendDir, name)) }
|
|
517
|
-
} catch {
|
|
518
|
-
// not this name — try the next
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
return { configFile: null, text: '' }
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* The `frontend-cache-components` row (`app` bucket). Since EC-515 the delivery kit's
|
|
526
|
-
* cache is a `'use cache'` entry, which Next compiles only with `cacheComponents: true`
|
|
527
|
-
* — without it `next build` fails under both bundlers (measured: "To use "use cache",
|
|
528
|
-
* please enable the feature flag `cacheComponents`"). So a missing flag is a `fail`:
|
|
529
|
-
* deterministic in the repo, and the build would fail anyway — so is a literal `false`. A
|
|
530
|
-
* `cacheComponents` key that is neither literal (computed from env, say) is a `warn` —
|
|
531
|
-
* the doctor reads text and cannot evaluate it. No frontend → no row. Pure.
|
|
532
|
-
*/
|
|
533
|
-
export function cacheComponentsDoctorChecks(facts, { frontendLabel = 'frontend' } = {}) {
|
|
534
|
-
if (!facts) return []
|
|
535
|
-
const base = { id: 'frontend-cache-components', bucket: 'app' }
|
|
536
|
-
const fix = `Set \`cacheComponents: true\` in ${frontendLabel}/next.config.mjs, and export \`generateStaticParams = route.generateStaticParams\` from the canvas catch-all page (see the upgrading notes for EC-515).`
|
|
537
|
-
if (facts.configFile === null) {
|
|
538
|
-
return [{ ...base, status: 'fail', detail: `${frontendLabel} has no next.config — the delivery kit needs \`cacheComponents: true\`.`, fix }]
|
|
539
|
-
}
|
|
540
|
-
const where = `${frontendLabel}/${facts.configFile}`
|
|
541
|
-
if (/\bcacheComponents\s*:\s*true\b/.test(facts.text)) {
|
|
542
|
-
return [{ ...base, status: 'ok', detail: `${where} enables \`cacheComponents\` (the kit's 'use cache' delivery cache).` }]
|
|
543
|
-
}
|
|
544
|
-
if (/\bcacheComponents\s*:\s*false\b/.test(facts.text)) {
|
|
545
|
-
return [{ ...base, status: 'fail', detail: `${where} sets \`cacheComponents: false\` — \`next build\` fails on the delivery kit's 'use cache' cache.`, fix }]
|
|
546
|
-
}
|
|
547
|
-
if (/\bcacheComponents\b/.test(facts.text)) {
|
|
548
|
-
return [
|
|
549
|
-
{
|
|
550
|
-
...base,
|
|
551
|
-
status: 'warn',
|
|
552
|
-
detail: `${where} sets \`cacheComponents\` to something other than the literal \`true\` — the doctor cannot evaluate it. \`next build\` fails if it is not true.`,
|
|
553
|
-
fix,
|
|
554
|
-
},
|
|
555
|
-
]
|
|
556
|
-
}
|
|
557
|
-
return [
|
|
558
|
-
{
|
|
559
|
-
...base,
|
|
560
|
-
status: 'fail',
|
|
561
|
-
detail: `${where} does not enable \`cacheComponents\` — \`next build\` fails on the delivery kit's 'use cache' cache.`,
|
|
562
|
-
fix,
|
|
478
|
+
...(project.locales ? { locales: project.locales } : {}),
|
|
563
479
|
},
|
|
564
|
-
|
|
480
|
+
...(snapshot?.status === 'answered'
|
|
481
|
+
? { redirects: snapshot.redirects }
|
|
482
|
+
: snapshot
|
|
483
|
+
? { redirectsUnavailable: `Redirects were not read from the deployment — ${snapshot.detail}` }
|
|
484
|
+
: {}),
|
|
485
|
+
}
|
|
565
486
|
}
|
|
566
487
|
|
|
567
488
|
// --- Command -----------------------------------------------------------------
|
|
@@ -621,11 +542,11 @@ export function cacheComponentsDoctorChecks(facts, { frontendLabel = 'frontend'
|
|
|
621
542
|
* resolved it (the routes the studio and delivery actually serve); the native
|
|
622
543
|
* `cms/index.ts` read is the fallback only when it could not, and the row says which.
|
|
623
544
|
*
|
|
624
|
-
* EC-
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
545
|
+
* EC-620: the doctor checks NO Next flag. Caching is the host's — the delivery kit holds
|
|
546
|
+
* no cache scope, so a frontend builds with `cacheComponents`, `experimental.useCache`
|
|
547
|
+
* or neither. (EC-515's `frontend-cache-components` row, which failed a frontend without
|
|
548
|
+
* `cacheComponents`, is deleted.) The engine's part — the invalidation signal — is the
|
|
549
|
+
* revalidate probe above.
|
|
629
550
|
*/
|
|
630
551
|
export async function runDoctor({
|
|
631
552
|
env = {},
|
|
@@ -642,7 +563,6 @@ export async function runDoctor({
|
|
|
642
563
|
routeFacts,
|
|
643
564
|
configOutcome,
|
|
644
565
|
installedEngineVersion,
|
|
645
|
-
frontendCacheComponents,
|
|
646
566
|
} = {}) {
|
|
647
567
|
const studioLabel = appDisplay(layout, 'studio')
|
|
648
568
|
const target = resolveDoctorTarget(env, { url, envSource: `${studioLabel}/.env` })
|
|
@@ -695,11 +615,18 @@ export async function runDoctor({
|
|
|
695
615
|
|
|
696
616
|
// EC-606: published documents of undeclared collections — one more request on the
|
|
697
617
|
// same public query API, made only when the config resolved (it needs the declared set).
|
|
618
|
+
// EC-590: the SAME snapshot read carries the deployment's redirects for the route checks.
|
|
619
|
+
const project = resolvedProjectOf(outcome)
|
|
620
|
+
const snapshot =
|
|
621
|
+
target?.kind === 'convex' && project && Array.isArray(project.collections)
|
|
622
|
+
? await probeDeliverySnapshot(target.url, project.slug, fetchImpl)
|
|
623
|
+
: undefined
|
|
698
624
|
const publishedOrphans = await gatherPublishedOrphans({
|
|
699
625
|
target,
|
|
700
|
-
project
|
|
626
|
+
project,
|
|
701
627
|
configFacts,
|
|
702
628
|
fetchImpl,
|
|
629
|
+
...(snapshot ? { probe: async () => snapshot } : {}),
|
|
703
630
|
})
|
|
704
631
|
if (publishedOrphans) facts.publishedOrphans = publishedOrphans
|
|
705
632
|
|
|
@@ -712,28 +639,19 @@ export async function runDoctor({
|
|
|
712
639
|
let checks = artifacts ? [...report.checks, ...previewsDoctorChecks(artifacts)] : [...report.checks]
|
|
713
640
|
|
|
714
641
|
// EC-603: the layout and AI-route rows, in the `app` bucket (both repo facts).
|
|
715
|
-
// EC-515: the frontend's `cacheComponents` flag — a repo fact, any backend mode.
|
|
716
|
-
const frontend = layout?.entries?.frontend
|
|
717
642
|
checks = spliceBucketChecks(checks, 'app', [
|
|
718
643
|
...layoutDoctorChecks(layout),
|
|
719
644
|
...aiRouteDoctorChecks(aiRoute, { studioLabel }),
|
|
720
|
-
...cacheComponentsDoctorChecks(
|
|
721
|
-
frontendCacheComponents ?? gatherFrontendCacheComponents(frontend?.found ? frontend.path : undefined),
|
|
722
|
-
{ frontendLabel: appDisplay(layout, 'frontend') },
|
|
723
|
-
),
|
|
724
645
|
])
|
|
725
646
|
|
|
726
|
-
// EC-469: the route
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
// `cms/index.ts` read is the fallback for when it could not (EC-495 had swapped in the
|
|
731
|
-
// resolved collections only, so the routes still came from `cms/index.ts`).
|
|
732
|
-
if (root !== undefined || routeFacts !== undefined) {
|
|
647
|
+
// EC-469/EC-590: the route checks — the one router's issue set over the resolved
|
|
648
|
+
// project (and the deployment's redirects when the snapshot was read), spliced into the
|
|
649
|
+
// `app` bucket. Routes are CODE, so the rows run in playground mode too.
|
|
650
|
+
if (root !== undefined || routeFacts !== undefined || configOutcome !== undefined) {
|
|
733
651
|
checks = spliceBucketChecks(
|
|
734
652
|
checks,
|
|
735
653
|
'app',
|
|
736
|
-
routesDoctorChecks(
|
|
654
|
+
routesDoctorChecks(routeFacts ?? routeFactsFor({ outcome, configFacts, snapshot })),
|
|
737
655
|
)
|
|
738
656
|
}
|
|
739
657
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The ONE "installed engine version" resolver of the CLI (EC-583, RESIDUE-4e) — read by
|
|
7
|
+
* `elytra doctor`/`init` (the `expected` half of the EC-552 version handshake) and by
|
|
8
|
+
* `elytra previews` (the version recorded on the manifest). The two used to resolve the
|
|
9
|
+
* same fact two ways: the doctor through the `@elytracms/studio/convex/delivery` export,
|
|
10
|
+
* previews through hand-joined `node_modules` paths with a silent `@elytracms/core`
|
|
11
|
+
* fallback.
|
|
12
|
+
*
|
|
13
|
+
* Resolved through the `convex/delivery` export and its adjacent `package.json`: that is
|
|
14
|
+
* the very source file the consumer's `convex deploy` bundles, so the version read here
|
|
15
|
+
* is by construction the version a redeploy would stamp. (The consumer's own
|
|
16
|
+
* `package.json` only carries a RANGE.) `undefined` when the studio's deps are not
|
|
17
|
+
* installed — callers report an honest `unknown`, never a guessed version.
|
|
18
|
+
*
|
|
19
|
+
* @param {string | undefined | null} studioDir the resolved studio app (EC-603 layout)
|
|
20
|
+
* @returns {string | undefined}
|
|
21
|
+
*/
|
|
22
|
+
export function resolveInstalledEngineVersion(studioDir) {
|
|
23
|
+
if (!studioDir) return undefined
|
|
24
|
+
try {
|
|
25
|
+
const req = createRequire(join(studioDir, 'package.json'))
|
|
26
|
+
const deliverySource = req.resolve('@elytracms/studio/convex/delivery')
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(dirname(deliverySource), '..', 'package.json'), 'utf8'))
|
|
28
|
+
return typeof pkg.version === 'string' ? pkg.version : undefined
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/previews.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync
|
|
|
3
3
|
import { dirname, isAbsolute, join, relative, sep } from 'node:path'
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
5
5
|
import { CONFIG_FILE, pickPort, resolveFromDir, startConsumerVite, virtualModulesPlugin } from './config-harness.js'
|
|
6
|
+
import { resolveInstalledEngineVersion } from './engine-version.js'
|
|
6
7
|
import { appDisplay, resolveProjectLayout } from './layout.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -558,28 +559,6 @@ export function previewsDoctorChecks(artifacts) {
|
|
|
558
559
|
|
|
559
560
|
// --- Impure plumbing ---------------------------------------------------------------
|
|
560
561
|
|
|
561
|
-
/**
|
|
562
|
-
* The engine version for the input hash — read from the `@elytracms/studio`
|
|
563
|
-
* (fallback `@elytracms/core`) package the CONSUMER has installed, never
|
|
564
|
-
* hardcoded. Direct node_modules paths (pnpm keeps symlinks for direct deps).
|
|
565
|
-
* `studioDir` is the resolved studio app (EC-603 layout).
|
|
566
|
-
*/
|
|
567
|
-
export function resolveEngineVersion(root, studioDir) {
|
|
568
|
-
const bases = [studioDir, root]
|
|
569
|
-
for (const pkg of ['@elytracms/studio', '@elytracms/core']) {
|
|
570
|
-
for (const base of bases) {
|
|
571
|
-
const path = join(base, 'node_modules', ...pkg.split('/'), 'package.json')
|
|
572
|
-
if (!existsSync(path)) continue
|
|
573
|
-
try {
|
|
574
|
-
const version = JSON.parse(readFileSync(path, 'utf8')).version
|
|
575
|
-
if (typeof version === 'string' && version.length > 0) return version
|
|
576
|
-
} catch {
|
|
577
|
-
// fall through to the next candidate
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return 'unknown'
|
|
582
|
-
}
|
|
583
562
|
|
|
584
563
|
/**
|
|
585
564
|
* The consumer theme entry the harness page imports: the studio's canvas
|
|
@@ -671,7 +650,7 @@ async function startHarnessServer({ root, studioDir, layout, port }) {
|
|
|
671
650
|
const tailwindEntry = resolveFromDir(studioDir, '@tailwindcss/vite')
|
|
672
651
|
const tailwind = (await import(pathToFileURL(tailwindEntry).href)).default
|
|
673
652
|
|
|
674
|
-
const configPath = join(root,
|
|
653
|
+
const configPath = join(root, CONFIG_FILE)
|
|
675
654
|
const themePath = themeStylesheetPath(root, layout)
|
|
676
655
|
if (!themePath) {
|
|
677
656
|
const studio = appDisplay(layout, 'studio')
|
|
@@ -813,7 +792,8 @@ export async function runPreviews(project, { check = false, port, log = (l) => c
|
|
|
813
792
|
const layout = project.layout ?? resolveProjectLayout(root)
|
|
814
793
|
const studioDir = layout.entries.studio.path
|
|
815
794
|
const studioLabel = layout.entries.studio.display
|
|
816
|
-
|
|
795
|
+
// EC-583 (RESIDUE-4e): the CLI's one installed-engine-version resolver.
|
|
796
|
+
const engine = resolveInstalledEngineVersion(studioDir) ?? 'unknown'
|
|
817
797
|
const manifestPath = join(studioDir, PREVIEWS_MANIFEST_FILE)
|
|
818
798
|
const imagesDir = join(studioDir, PREVIEWS_DIR)
|
|
819
799
|
|
package/src/project.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
|
+
import { WORKSPACE_CONFIG_FILE } from '@elytracms/core/config'
|
|
3
4
|
import { parseBackendMode } from '@elytracms/core/export-sync/deployment-ready'
|
|
4
5
|
import { appDisplay, resolveProjectLayout } from './layout.js'
|
|
5
6
|
|
|
@@ -34,7 +35,7 @@ import { appDisplay, resolveProjectLayout } from './layout.js'
|
|
|
34
35
|
export function findRepoRoot(start) {
|
|
35
36
|
let dir = start
|
|
36
37
|
for (;;) {
|
|
37
|
-
if (existsSync(join(dir,
|
|
38
|
+
if (existsSync(join(dir, WORKSPACE_CONFIG_FILE))) return dir
|
|
38
39
|
const parent = dirname(dir)
|
|
39
40
|
if (parent === dir) return null
|
|
40
41
|
dir = parent
|
package/src/provision.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
2
|
import { generateKeyPairSync } from 'node:crypto'
|
|
3
|
+
import { AUTH_SIGNING_KEY_ENV } from '@elytracms/core/export-sync/deployment-ready'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* First-run auto-provisioning for the Elytra CLI (EC-259). Folds the Convex Auth
|
|
@@ -42,8 +43,8 @@ export function generateAuthKeys() {
|
|
|
42
43
|
const jwk = publicKey.export({ format: 'jwk' })
|
|
43
44
|
return {
|
|
44
45
|
// Match @convex-dev/auth: PKCS#8 PEM, trailing whitespace trimmed, newlines → spaces.
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
[AUTH_SIGNING_KEY_ENV.privateKey]: pkcs8.trimEnd().replace(/\n/g, ' '),
|
|
47
|
+
[AUTH_SIGNING_KEY_ENV.publicKeys]: JSON.stringify({ keys: [{ use: 'sig', ...jwk }] }),
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -83,8 +84,8 @@ export function convexEnvSet(name, value, { cwd, exec = defaultConvexExec } = {}
|
|
|
83
84
|
/** Which of the two auth signing-key vars are already present on the deployment. */
|
|
84
85
|
export function detectKeyState({ cwd, exec } = {}) {
|
|
85
86
|
return {
|
|
86
|
-
jwt: convexEnvGet(
|
|
87
|
-
jwks: convexEnvGet(
|
|
87
|
+
jwt: convexEnvGet(AUTH_SIGNING_KEY_ENV.privateKey, { cwd, exec }) !== '',
|
|
88
|
+
jwks: convexEnvGet(AUTH_SIGNING_KEY_ENV.publicKeys, { cwd, exec }) !== '',
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -110,8 +111,9 @@ export function ensureAuthKeys({ cwd, exec, log = () => {}, generate = generateA
|
|
|
110
111
|
return { action: 'partial', state }
|
|
111
112
|
}
|
|
112
113
|
const keys = generate()
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
for (const name of [AUTH_SIGNING_KEY_ENV.privateKey, AUTH_SIGNING_KEY_ENV.publicKeys]) {
|
|
115
|
+
convexEnvSet(name, keys[name], { cwd, exec })
|
|
116
|
+
}
|
|
115
117
|
log('✓ Generated and set the Convex Auth signing keys (JWT_PRIVATE_KEY + JWKS).')
|
|
116
118
|
return { action: 'generated', state }
|
|
117
119
|
}
|