@elytracms/cli 0.0.77 → 0.0.79
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/README.md +15 -0
- package/package.json +2 -2
- package/src/cli.js +138 -7
- package/src/project.js +22 -2
- package/src/run.js +5 -2
package/README.md
CHANGED
|
@@ -56,6 +56,11 @@ elytra version Show version
|
|
|
56
56
|
|
|
57
57
|
Run from anywhere inside an Elytra project (a directory with `elytra.config.ts`).
|
|
58
58
|
|
|
59
|
+
Every subcommand accepts `--help`/`-h` and prints its own usage — inertly: no
|
|
60
|
+
project inspection, no process spawned, no network call, no write. `elytra dev
|
|
61
|
+
--help` never starts a server; `elytra doctor --help` never runs the network
|
|
62
|
+
diagnostic; `elytra build --help` never validates a target.
|
|
63
|
+
|
|
59
64
|
## Mode-aware
|
|
60
65
|
|
|
61
66
|
The CLI reads the studio's `.env` (the same backend signal the studio itself
|
|
@@ -70,6 +75,16 @@ It orchestrates and owns the process lifecycle; it does not reimplement Vite,
|
|
|
70
75
|
Next, or Convex — each step shells out to the underlying tool and inherits its
|
|
71
76
|
watch/HMR/build behaviour.
|
|
72
77
|
|
|
78
|
+
## Dev ports
|
|
79
|
+
|
|
80
|
+
The studio defaults to `:5180`, the frontend to `:3000`. One lever moves both
|
|
81
|
+
together — set `ELYTRA_DEV_PORT_OFFSET` in `studio/.env` (e.g. `100` → studio
|
|
82
|
+
`:5280`, frontend `:3100`) to run a second Elytra project alongside this one
|
|
83
|
+
without a clash. `elytra dev` reads it from `studio/.env` and forwards it to
|
|
84
|
+
both child processes; the studio's own `vite.config.ts` also reads it directly
|
|
85
|
+
(so `vite dev` run standalone honors it too). Unset (the default) leaves both
|
|
86
|
+
ports exactly as they are today.
|
|
87
|
+
|
|
73
88
|
## Deploying (Vercel)
|
|
74
89
|
|
|
75
90
|
Deploys are git-push-driven — there is no `elytra deploy`. A consuming repo
|
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.79",
|
|
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": ">=20"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@elytracms/core": "^0.0.
|
|
20
|
+
"@elytracms/core": "^0.0.79"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"vitest": "^4.1.10"
|
package/src/cli.js
CHANGED
|
@@ -44,12 +44,124 @@ Commands:
|
|
|
44
44
|
version Show version
|
|
45
45
|
|
|
46
46
|
Run from anywhere inside an Elytra project (a directory with elytra.config.ts).
|
|
47
|
+
Any subcommand accepts --help/-h for its own usage (never does the command's work).
|
|
48
|
+
|
|
49
|
+
Dev ports: studio 5180, frontend 3000 by default. Set ELYTRA_DEV_PORT_OFFSET in
|
|
50
|
+
studio/.env to shift BOTH together (e.g. to run a second Elytra project alongside
|
|
51
|
+
this one without a clash) — 'elytra dev' forwards it to both processes.
|
|
47
52
|
`.trimStart()
|
|
48
53
|
|
|
49
54
|
function log(line = '') {
|
|
50
55
|
stdout.write(`${line}\n`)
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
// --- Uniform inert `--help`/`-h` (EC-464) -------------------------------------
|
|
59
|
+
//
|
|
60
|
+
// Two builders (one external) independently hit `elytra <subcommand> --help`
|
|
61
|
+
// DOING the subcommand's work instead of just describing it — `dev --help`
|
|
62
|
+
// started real dev servers (attempting the shared default ports, 5180/3000),
|
|
63
|
+
// `doctor --help` ran the full network diagnostic, `build --help` rejected as
|
|
64
|
+
// an unknown build target. The fix is one central gate in `run()`, checked
|
|
65
|
+
// BEFORE any subcommand branch does anything: every entry in `SUBCOMMAND_HELP`
|
|
66
|
+
// is inert under `--help`/`-h` by construction — no project inspection, no
|
|
67
|
+
// process spawned, no network call, no write. A future subcommand inherits
|
|
68
|
+
// this for free by adding its usage text here; nothing per-command to
|
|
69
|
+
// remember.
|
|
70
|
+
|
|
71
|
+
/** Per-subcommand usage text — each documents that subcommand's real flags. */
|
|
72
|
+
export const SUBCOMMAND_HELP = {
|
|
73
|
+
dev: `
|
|
74
|
+
Usage: elytra dev
|
|
75
|
+
|
|
76
|
+
Start the studio + frontend (and Convex in connected mode) together as one
|
|
77
|
+
supervised process group. One Ctrl-C stops everything; if any process exits
|
|
78
|
+
on its own, the rest are torn down.
|
|
79
|
+
|
|
80
|
+
No flags. The dev ports (studio 5180, frontend 3000) move together via the
|
|
81
|
+
ELYTRA_DEV_PORT_OFFSET lever — see studio/.env.
|
|
82
|
+
`.trim(),
|
|
83
|
+
|
|
84
|
+
build: `
|
|
85
|
+
Usage: elytra build [target]
|
|
86
|
+
|
|
87
|
+
Build the studio + frontend (and deploy Convex in connected mode), in order,
|
|
88
|
+
failing as a whole if any step fails.
|
|
89
|
+
|
|
90
|
+
Optional target — the per-app Vercel build commands (EC-410):
|
|
91
|
+
elytra build studio Convex deploy coupled with the studio build
|
|
92
|
+
(functions + client stay in lockstep)
|
|
93
|
+
elytra build frontend frontend build only
|
|
94
|
+
|
|
95
|
+
No target builds the whole repo in order: convex deploy (connected mode) →
|
|
96
|
+
studio build → frontend build.
|
|
97
|
+
`.trim(),
|
|
98
|
+
|
|
99
|
+
init: `
|
|
100
|
+
Usage: elytra init
|
|
101
|
+
|
|
102
|
+
First-run setup: provision Convex, generate the auth signing keys, verify
|
|
103
|
+
readiness. Idempotent — safe to re-run at any point. No flags.
|
|
104
|
+
`.trim(),
|
|
105
|
+
|
|
106
|
+
doctor: `
|
|
107
|
+
Usage: elytra doctor [--url <convex url>]
|
|
108
|
+
|
|
109
|
+
Diagnose the active deployment's readiness (URL, reachability, auth signing
|
|
110
|
+
keys, expected deployment env vars — set/missing by NAME, values never shown
|
|
111
|
+
— config, membership) and print each fix. When the instant-publish
|
|
112
|
+
revalidate pair is set, also sends a signed no-op ping to the endpoint to
|
|
113
|
+
prove the publish → live chain end to end. Also reports the
|
|
114
|
+
generated-previews artifacts (manifest/files present; hash staleness lives
|
|
115
|
+
in \`elytra previews --check\`). Exit non-zero if any check fails. Read-only.
|
|
116
|
+
|
|
117
|
+
Options:
|
|
118
|
+
--url <convex url> Diagnose this deployment instead of the one resolved
|
|
119
|
+
from studio/.env.
|
|
120
|
+
`.trim(),
|
|
121
|
+
|
|
122
|
+
previews: `
|
|
123
|
+
Usage: elytra previews [--check] [--port <n>]
|
|
124
|
+
|
|
125
|
+
Render every canvas-eligible component's fixtures against YOUR theme
|
|
126
|
+
(headless Chromium via Playwright — an optional install) and write
|
|
127
|
+
committed preview images (studio/public/elytra-previews/) plus an
|
|
128
|
+
importable manifest (studio/elytra-previews.json) for the insert menu.
|
|
129
|
+
Re-running with unchanged inputs rewrites nothing.
|
|
130
|
+
|
|
131
|
+
Options:
|
|
132
|
+
--check Verify freshness (input hashes) without rendering; exit
|
|
133
|
+
non-zero when previews are missing/stale.
|
|
134
|
+
--port <n> Harness dev-server port (default: first free 5440+).
|
|
135
|
+
`.trim(),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Does this subcommand's own argv (everything after argv[0]) ask for help? */
|
|
139
|
+
function hasHelpFlag(subArgs) {
|
|
140
|
+
return subArgs.includes('--help') || subArgs.includes('-h')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Pure dispatch decision (EC-464): does this invocation want top-level help,
|
|
145
|
+
* a subcommand's inert help, the version, or should the subcommand actually
|
|
146
|
+
* run? Centralizes the `--help`/`-h` gate so every subcommand is inert under
|
|
147
|
+
* help BY CONSTRUCTION — table-testable directly, independent of `run()`'s
|
|
148
|
+
* process-spawning/network/fs side effects. The regression this pins is
|
|
149
|
+
* "`--help` did real work".
|
|
150
|
+
*/
|
|
151
|
+
export function decideAction(argv) {
|
|
152
|
+
const command = argv[0] ?? 'help'
|
|
153
|
+
if (command === 'help' || command === '-h' || command === '--help') {
|
|
154
|
+
return { action: 'help', subcommand: 'help' }
|
|
155
|
+
}
|
|
156
|
+
if (command === 'version' || command === '--version') {
|
|
157
|
+
return { action: 'version', subcommand: 'version' }
|
|
158
|
+
}
|
|
159
|
+
if (Object.hasOwn(SUBCOMMAND_HELP, command) && hasHelpFlag(argv.slice(1))) {
|
|
160
|
+
return { action: 'help', subcommand: command }
|
|
161
|
+
}
|
|
162
|
+
return { action: 'run', subcommand: command }
|
|
163
|
+
}
|
|
164
|
+
|
|
53
165
|
function reportIssues(issues) {
|
|
54
166
|
log('Cannot continue:')
|
|
55
167
|
for (const issue of issues) log(` • ${issue}`)
|
|
@@ -61,11 +173,28 @@ function reportIssues(issues) {
|
|
|
61
173
|
* ever added for a provisioned, non-prod deployment: against a `prod:` selector (EC-356)
|
|
62
174
|
* it is never started, prod-safety-critical, regardless of how the studio/frontend are
|
|
63
175
|
* configured. `describeMode` explains why to the developer.
|
|
176
|
+
*
|
|
177
|
+
* EC-464 dev-port lever: `project.devPortOffset` (resolved by `inspectProject` from
|
|
178
|
+
* studio/.env's `ELYTRA_DEV_PORT_OFFSET`) is forwarded as an env var to BOTH the
|
|
179
|
+
* studio and frontend child processes — the studio's own `vite.config.ts` reads it
|
|
180
|
+
* directly (so it also applies when running `vite dev` standalone), and the
|
|
181
|
+
* frontend's `dev` script reads it from its process env (which only `elytra dev`
|
|
182
|
+
* can supply, since dotenv files aren't shell-sourced) — one lever, moves both.
|
|
64
183
|
*/
|
|
65
184
|
export function planDev(project) {
|
|
66
185
|
const procs = []
|
|
67
186
|
const studioDir = join(project.root, 'studio')
|
|
68
187
|
const frontendDir = join(project.root, 'frontend')
|
|
188
|
+
const offset = Number(project.devPortOffset ?? 0) || 0
|
|
189
|
+
// The studio's `vite.config.ts` resolves the offset itself, in JS (`loadEnv`) —
|
|
190
|
+
// portable anywhere, so forward the lever as-is.
|
|
191
|
+
const studioEnv = { ELYTRA_DEV_PORT_OFFSET: String(offset) }
|
|
192
|
+
// The frontend's port is resolved HERE instead, and handed over as `PORT` (which
|
|
193
|
+
// Next reads from the process env natively). That keeps the scaffold's `dev`
|
|
194
|
+
// script a plain `next dev`: computing the port inside the npm script needs POSIX
|
|
195
|
+
// arithmetic (`$(( ))`), which silently breaks `pnpm dev` under cmd.exe on
|
|
196
|
+
// Windows — every other template script is shell-agnostic and this one stays so.
|
|
197
|
+
const frontendEnv = { ELYTRA_DEV_PORT_OFFSET: String(offset), PORT: String(3000 + offset) }
|
|
69
198
|
|
|
70
199
|
if (project.apps.studio && project.mode === 'connected') {
|
|
71
200
|
if (project.convexProvisioned && !project.isProdDeployment) {
|
|
@@ -73,10 +202,10 @@ export function planDev(project) {
|
|
|
73
202
|
}
|
|
74
203
|
}
|
|
75
204
|
if (project.apps.studio) {
|
|
76
|
-
procs.push({ name: 'studio', command: 'pnpm', args: ['run', 'dev'], cwd: studioDir })
|
|
205
|
+
procs.push({ name: 'studio', command: 'pnpm', args: ['run', 'dev'], cwd: studioDir, env: studioEnv })
|
|
77
206
|
}
|
|
78
207
|
if (project.apps.frontend) {
|
|
79
|
-
procs.push({ name: 'frontend', command: 'pnpm', args: ['run', 'dev'], cwd: frontendDir })
|
|
208
|
+
procs.push({ name: 'frontend', command: 'pnpm', args: ['run', 'dev'], cwd: frontendDir, env: frontendEnv })
|
|
80
209
|
}
|
|
81
210
|
return procs
|
|
82
211
|
}
|
|
@@ -308,19 +437,21 @@ export async function runInit(project, { log: emit = log, fetchImpl, exec, provi
|
|
|
308
437
|
}
|
|
309
438
|
|
|
310
439
|
export async function run(argv, { cwd = process.cwd(), env = process.env, emit = log, exec, fetchImpl } = {}) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
440
|
+
// EC-464: the ONE gate — decide before touching fs/network/processes. `help` and
|
|
441
|
+
// every per-subcommand `--help`/`-h` short-circuit here, inertly, by construction.
|
|
442
|
+
const decision = decideAction(argv)
|
|
443
|
+
if (decision.action === 'help') {
|
|
444
|
+
log(decision.subcommand === 'help' ? HELP : SUBCOMMAND_HELP[decision.subcommand])
|
|
315
445
|
return 0
|
|
316
446
|
}
|
|
317
|
-
if (
|
|
447
|
+
if (decision.action === 'version') {
|
|
318
448
|
const { readFileSync } = await import('node:fs')
|
|
319
449
|
const { fileURLToPath } = await import('node:url')
|
|
320
450
|
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url))
|
|
321
451
|
log(JSON.parse(readFileSync(pkgPath, 'utf8')).version)
|
|
322
452
|
return 0
|
|
323
453
|
}
|
|
454
|
+
const command = decision.subcommand
|
|
324
455
|
|
|
325
456
|
// `doctor` is a read-only diagnostic: it needs the repo root + env + network, but not
|
|
326
457
|
// installed deps — so it is not blocked by a missing node_modules.
|
package/src/project.js
CHANGED
|
@@ -92,10 +92,26 @@ export function isProdDeploymentSelector(env = {}) {
|
|
|
92
92
|
)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Parse the EC-464 dev-port-offset lever from the resolved studio env: a single
|
|
97
|
+
* integer that shifts BOTH the studio's and the frontend's dev port together (the
|
|
98
|
+
* studio's own `vite.config.ts` reads it directly; `elytra dev` forwards it to the
|
|
99
|
+
* frontend child too, since a dotenv file isn't shell-sourced for its `next dev`
|
|
100
|
+
* script) — one lever, so a second Elytra project can run alongside this one
|
|
101
|
+
* without a port clash. Unset/blank/non-numeric → 0, the current 5180/3000
|
|
102
|
+
* defaults, byte-for-byte unchanged for every existing scaffold.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveDevPortOffset(env = {}) {
|
|
105
|
+
const raw = env.ELYTRA_DEV_PORT_OFFSET
|
|
106
|
+
if (raw === undefined || String(raw).trim() === '') return 0
|
|
107
|
+
const n = Number(raw)
|
|
108
|
+
return Number.isFinite(n) ? Math.trunc(n) : 0
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
/**
|
|
96
112
|
* Inspect a consuming repo. Returns:
|
|
97
113
|
* { root, apps: {studio, frontend}, mode, convexProvisioned, isProdDeployment,
|
|
98
|
-
* hasDeployKey, hasAiRoute, issues }
|
|
114
|
+
* hasDeployKey, hasAiRoute, devPortOffset, issues }
|
|
99
115
|
* `issues` are actionable, human-readable prerequisite problems (empty = ok).
|
|
100
116
|
*/
|
|
101
117
|
export function inspectProject(cwd, processEnv = {}) {
|
|
@@ -109,6 +125,7 @@ export function inspectProject(cwd, processEnv = {}) {
|
|
|
109
125
|
isProdDeployment: false,
|
|
110
126
|
hasDeployKey: false,
|
|
111
127
|
hasAiRoute: false,
|
|
128
|
+
devPortOffset: 0,
|
|
112
129
|
issues: [
|
|
113
130
|
'No elytra.config.ts found in this directory or any parent. Run `elytra` from inside an Elytra project (or scaffold one with `npm create elytra@latest`).',
|
|
114
131
|
],
|
|
@@ -151,5 +168,8 @@ export function inspectProject(cwd, processEnv = {}) {
|
|
|
151
168
|
// `createAiDevPlugin` default path); we never execute config to find out.
|
|
152
169
|
const hasAiRoute = apps.studio && existsSync(join(root, 'studio', 'src', 'server', 'ai-handler.ts'))
|
|
153
170
|
|
|
154
|
-
|
|
171
|
+
// EC-464: the one dev-port lever, read from the same studio env as everything else here.
|
|
172
|
+
const devPortOffset = resolveDevPortOffset(studioEnv)
|
|
173
|
+
|
|
174
|
+
return { root, apps, mode, convexProvisioned, isProdDeployment, hasDeployKey, hasAiRoute, devPortOffset, issues }
|
|
155
175
|
}
|
package/src/run.js
CHANGED
|
@@ -34,7 +34,10 @@ function pipePrefixed(stream, label, color) {
|
|
|
34
34
|
* Ctrl-C (SIGINT/SIGTERM) tears all of them down; if any child exits on its own,
|
|
35
35
|
* the rest are killed and the group resolves with that child's exit code.
|
|
36
36
|
*
|
|
37
|
-
* `procs`: [{ name, command, args, cwd }]
|
|
37
|
+
* `procs`: [{ name, command, args, cwd, env? }] — an optional `env` is merged
|
|
38
|
+
* OVER `process.env` for that one child only (EC-464: how `planDev` forwards the
|
|
39
|
+
* dev-port-offset lever to the studio/frontend children without changing every
|
|
40
|
+
* other process's environment). Returns the group's exit code.
|
|
38
41
|
*/
|
|
39
42
|
export function runGroup(procs) {
|
|
40
43
|
return new Promise((resolve) => {
|
|
@@ -69,7 +72,7 @@ export function runGroup(procs) {
|
|
|
69
72
|
stdout.write(`${paint(`[${proc.name}]`, color)} ${proc.command} ${proc.args.join(' ')}\n`)
|
|
70
73
|
const child = spawn(proc.command, proc.args, {
|
|
71
74
|
cwd: proc.cwd,
|
|
72
|
-
env: process.env,
|
|
75
|
+
env: proc.env ? { ...process.env, ...proc.env } : process.env,
|
|
73
76
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
77
|
})
|
|
75
78
|
children.push(child)
|