@elytracms/cli 0.0.1
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/LICENSE +21 -0
- package/README.md +38 -0
- package/index.js +10 -0
- package/package.json +26 -0
- package/src/cli.js +133 -0
- package/src/project.js +98 -0
- package/src/run.js +132 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 finaldream
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @elytracms/cli
|
|
2
|
+
|
|
3
|
+
The Elytra CLI — run an [Elytra](https://www.npmjs.com/package/@elytracms/core)
|
|
4
|
+
consuming repo's studio, delivery frontend, and Convex backend under one command.
|
|
5
|
+
|
|
6
|
+
Installed as a devDependency in projects scaffolded by
|
|
7
|
+
[`create-elytra`](https://www.npmjs.com/package/create-elytra); its `dev`/`build`
|
|
8
|
+
scripts call `elytra`.
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
elytra dev Start the studio + frontend (and Convex in connected mode)
|
|
14
|
+
together as one supervised process group. One Ctrl-C stops
|
|
15
|
+
everything; if any process exits, the rest are torn down.
|
|
16
|
+
|
|
17
|
+
elytra build Build the studio + frontend (and deploy Convex in connected
|
|
18
|
+
mode), in order — failing as a whole if any step fails.
|
|
19
|
+
|
|
20
|
+
elytra help Show help
|
|
21
|
+
elytra version Show version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run from anywhere inside an Elytra project (a directory with `elytra.config.ts`).
|
|
25
|
+
|
|
26
|
+
## Mode-aware
|
|
27
|
+
|
|
28
|
+
The CLI reads the studio's `.env` (the same backend signal the studio itself
|
|
29
|
+
uses):
|
|
30
|
+
|
|
31
|
+
- **Playground** (`VITE_ELYTRA_MODE=playground`) — runs the studio + frontend
|
|
32
|
+
only; no Convex.
|
|
33
|
+
- **Connected** (`VITE_CONVEX_URL` set) — also runs `convex dev` (or deploys on
|
|
34
|
+
build) once a deployment is provisioned (`CONVEX_DEPLOYMENT` present).
|
|
35
|
+
|
|
36
|
+
It orchestrates and owns the process lifecycle; it does not reimplement Vite,
|
|
37
|
+
Next, or Convex — each step shells out to the underlying tool and inherits its
|
|
38
|
+
watch/HMR/build behaviour.
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elytracms/cli",
|
|
3
|
+
"author": "finaldream <hi@finaldream.de>",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "The Elytra CLI — run a consuming repo's studio, frontend, and Convex backend under one command.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"elytra": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"vitest": "^3.0.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"start": "node ./index.js",
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { stdout } from 'node:process'
|
|
3
|
+
import { inspectProject } from './project.js'
|
|
4
|
+
import { runGroup, runSequential } from './run.js'
|
|
5
|
+
|
|
6
|
+
const HELP = `
|
|
7
|
+
elytra — run an Elytra project's studio, frontend, and backend under one command
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
elytra <command>
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
dev Start the studio + frontend (and Convex in connected mode) together.
|
|
14
|
+
One Ctrl-C stops everything.
|
|
15
|
+
build Build the studio + frontend (and deploy Convex in connected mode),
|
|
16
|
+
in order, failing as a whole if any step fails.
|
|
17
|
+
help Show this help
|
|
18
|
+
version Show version
|
|
19
|
+
|
|
20
|
+
Run from anywhere inside an Elytra project (a directory with elytra.config.ts).
|
|
21
|
+
`.trimStart()
|
|
22
|
+
|
|
23
|
+
function log(line = '') {
|
|
24
|
+
stdout.write(`${line}\n`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function reportIssues(issues) {
|
|
28
|
+
log('Cannot continue:')
|
|
29
|
+
for (const issue of issues) log(` • ${issue}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build the dev process group from a project inspection. */
|
|
33
|
+
export function planDev(project) {
|
|
34
|
+
const procs = []
|
|
35
|
+
const studioDir = join(project.root, 'studio')
|
|
36
|
+
const frontendDir = join(project.root, 'frontend')
|
|
37
|
+
|
|
38
|
+
if (project.apps.studio && project.mode === 'connected') {
|
|
39
|
+
if (project.convexProvisioned) {
|
|
40
|
+
procs.push({ name: 'convex', command: 'pnpm', args: ['run', 'convex:dev'], cwd: studioDir })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (project.apps.studio) {
|
|
44
|
+
procs.push({ name: 'studio', command: 'pnpm', args: ['run', 'dev'], cwd: studioDir })
|
|
45
|
+
}
|
|
46
|
+
if (project.apps.frontend) {
|
|
47
|
+
procs.push({ name: 'frontend', command: 'pnpm', args: ['run', 'dev'], cwd: frontendDir })
|
|
48
|
+
}
|
|
49
|
+
return procs
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build the ordered build steps from a project inspection. */
|
|
53
|
+
export function planBuild(project) {
|
|
54
|
+
const steps = []
|
|
55
|
+
const studioDir = join(project.root, 'studio')
|
|
56
|
+
const frontendDir = join(project.root, 'frontend')
|
|
57
|
+
|
|
58
|
+
if (project.mode === 'connected' && project.convexProvisioned) {
|
|
59
|
+
steps.push({ name: 'convex deploy', command: 'pnpm', args: ['exec', 'convex', 'deploy'], cwd: studioDir })
|
|
60
|
+
}
|
|
61
|
+
if (project.apps.studio) {
|
|
62
|
+
steps.push({ name: 'studio build', command: 'pnpm', args: ['run', 'build'], cwd: studioDir })
|
|
63
|
+
}
|
|
64
|
+
if (project.apps.frontend) {
|
|
65
|
+
steps.push({ name: 'frontend build', command: 'pnpm', args: ['run', 'build'], cwd: frontendDir })
|
|
66
|
+
}
|
|
67
|
+
return steps
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function noteMode(project) {
|
|
71
|
+
if (project.mode === 'playground') {
|
|
72
|
+
log('Mode: playground (in-memory, no backend). Convex is not started.')
|
|
73
|
+
} else if (project.mode === 'connected' && !project.convexProvisioned) {
|
|
74
|
+
log(
|
|
75
|
+
'Mode: connected, but no Convex deployment is provisioned yet. Skipping the\n' +
|
|
76
|
+
'backend — run `cd studio && npx convex dev` once to provision it. See CONNECT.md.',
|
|
77
|
+
)
|
|
78
|
+
} else if (project.mode === 'unconfigured') {
|
|
79
|
+
log(
|
|
80
|
+
'Backend not configured. Defaulting to the frontends only. Set VITE_ELYTRA_MODE=\n' +
|
|
81
|
+
'playground or VITE_CONVEX_URL in studio/.env. See CONNECT.md.',
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function run(argv, { cwd = process.cwd(), env = process.env } = {}) {
|
|
87
|
+
const command = argv[0] ?? 'help'
|
|
88
|
+
|
|
89
|
+
if (command === 'help' || command === '-h' || command === '--help') {
|
|
90
|
+
log(HELP)
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
if (command === 'version' || command === '--version') {
|
|
94
|
+
const { readFileSync } = await import('node:fs')
|
|
95
|
+
const { fileURLToPath } = await import('node:url')
|
|
96
|
+
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url))
|
|
97
|
+
log(JSON.parse(readFileSync(pkgPath, 'utf8')).version)
|
|
98
|
+
return 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (command !== 'dev' && command !== 'build') {
|
|
102
|
+
log(`Unknown command: ${command}\n`)
|
|
103
|
+
log(HELP)
|
|
104
|
+
return 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const project = inspectProject(cwd, env)
|
|
108
|
+
if (project.issues.length > 0) {
|
|
109
|
+
reportIssues(project.issues)
|
|
110
|
+
return 1
|
|
111
|
+
}
|
|
112
|
+
noteMode(project)
|
|
113
|
+
|
|
114
|
+
if (command === 'dev') {
|
|
115
|
+
const procs = planDev(project)
|
|
116
|
+
if (procs.length === 0) {
|
|
117
|
+
log('Nothing to run — no studio/ or frontend/ app found.')
|
|
118
|
+
return 1
|
|
119
|
+
}
|
|
120
|
+
log('Starting (Ctrl-C to stop everything)…\n')
|
|
121
|
+
return runGroup(procs)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// build
|
|
125
|
+
const steps = planBuild(project)
|
|
126
|
+
if (steps.length === 0) {
|
|
127
|
+
log('Nothing to build — no studio/ or frontend/ app found.')
|
|
128
|
+
return 1
|
|
129
|
+
}
|
|
130
|
+
const code = await runSequential(steps)
|
|
131
|
+
log(code === 0 ? '\n✓ Build complete.' : '\n✗ Build failed.')
|
|
132
|
+
return code
|
|
133
|
+
}
|
package/src/project.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Project detection for the Elytra CLI (EC-235). Convention-aware, not config-
|
|
6
|
+
* executing: a consuming repo's layout (`studio/`, `frontend/`, root
|
|
7
|
+
* `elytra.config.ts`) and its `.env` are the source of truth — the same `.env`
|
|
8
|
+
* the studio itself reads to resolve its backend (`VITE_ELYTRA_MODE` /
|
|
9
|
+
* `VITE_CONVEX_URL`). We deliberately do NOT load the TS config (no TS runtime
|
|
10
|
+
* dependency); the paths are conventional and the operative backend signal lives
|
|
11
|
+
* in env regardless.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Walk up from `start` until a directory containing `elytra.config.ts` is found. */
|
|
15
|
+
export function findRepoRoot(start) {
|
|
16
|
+
let dir = start
|
|
17
|
+
for (;;) {
|
|
18
|
+
if (existsSync(join(dir, 'elytra.config.ts'))) return dir
|
|
19
|
+
const parent = dirname(dir)
|
|
20
|
+
if (parent === dir) return null
|
|
21
|
+
dir = parent
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Parse a dotenv file into a flat record. Missing file → {}. */
|
|
26
|
+
function readEnvFile(path) {
|
|
27
|
+
if (!existsSync(path)) return {}
|
|
28
|
+
const out = {}
|
|
29
|
+
for (const rawLine of readFileSync(path, 'utf8').split('\n')) {
|
|
30
|
+
const line = rawLine.trim()
|
|
31
|
+
if (!line || line.startsWith('#')) continue
|
|
32
|
+
const eq = line.indexOf('=')
|
|
33
|
+
if (eq === -1) continue
|
|
34
|
+
const key = line.slice(0, eq).trim()
|
|
35
|
+
let value = line.slice(eq + 1).trim()
|
|
36
|
+
// Strip an inline comment and surrounding quotes.
|
|
37
|
+
value = value.replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '')
|
|
38
|
+
out[key] = value
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Merge a package's `.env` then `.env.local` (local wins), then process.env. */
|
|
44
|
+
function resolveEnv(appDir, processEnv) {
|
|
45
|
+
return {
|
|
46
|
+
...readEnvFile(join(appDir, '.env')),
|
|
47
|
+
...readEnvFile(join(appDir, '.env.local')),
|
|
48
|
+
...processEnv,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isApp(root, name) {
|
|
53
|
+
return existsSync(join(root, name, 'package.json'))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Inspect a consuming repo. Returns:
|
|
58
|
+
* { root, apps: {studio, frontend}, mode, convexProvisioned, issues }
|
|
59
|
+
* `issues` are actionable, human-readable prerequisite problems (empty = ok).
|
|
60
|
+
*/
|
|
61
|
+
export function inspectProject(cwd, processEnv = {}) {
|
|
62
|
+
const root = findRepoRoot(cwd)
|
|
63
|
+
if (!root) {
|
|
64
|
+
return {
|
|
65
|
+
root: null,
|
|
66
|
+
apps: { studio: false, frontend: false },
|
|
67
|
+
mode: 'unknown',
|
|
68
|
+
convexProvisioned: false,
|
|
69
|
+
issues: [
|
|
70
|
+
'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`).',
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const apps = { studio: isApp(root, 'studio'), frontend: isApp(root, 'frontend') }
|
|
76
|
+
const issues = []
|
|
77
|
+
if (!apps.studio && !apps.frontend) {
|
|
78
|
+
issues.push('No `studio/` or `frontend/` app found next to elytra.config.ts.')
|
|
79
|
+
}
|
|
80
|
+
if (!existsSync(join(root, 'node_modules'))) {
|
|
81
|
+
issues.push('Dependencies are not installed. Run `pnpm install` first.')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Backend mode is read from the studio app's env, mirroring the studio's own
|
|
85
|
+
// resolveBackendConfig: explicit playground flag wins; otherwise a Convex URL
|
|
86
|
+
// means connected; neither means it still needs configuring.
|
|
87
|
+
const studioEnv = apps.studio ? resolveEnv(join(root, 'studio'), processEnv) : processEnv
|
|
88
|
+
let mode
|
|
89
|
+
if (studioEnv.VITE_ELYTRA_MODE === 'playground') mode = 'playground'
|
|
90
|
+
else if (studioEnv.VITE_CONVEX_URL) mode = 'connected'
|
|
91
|
+
else mode = 'unconfigured'
|
|
92
|
+
|
|
93
|
+
// A Convex dev/deploy step only makes sense once a deployment is provisioned
|
|
94
|
+
// (`npx convex dev` writes CONVEX_DEPLOYMENT). Otherwise we skip it and say so.
|
|
95
|
+
const convexProvisioned = Boolean(studioEnv.CONVEX_DEPLOYMENT)
|
|
96
|
+
|
|
97
|
+
return { root, apps, mode, convexProvisioned, issues }
|
|
98
|
+
}
|
package/src/run.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { stdout } from 'node:process'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Process orchestration for the Elytra CLI (EC-235). We own the lifecycle but
|
|
6
|
+
* never reimplement Vite/Next/Convex — each step shells out to the underlying
|
|
7
|
+
* tool and inherits its watch/HMR/build behaviour.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const COLORS = ['36', '35', '33', '32', '34'] // cyan, magenta, yellow, green, blue
|
|
11
|
+
const useColor = !process.env.NO_COLOR && stdout.isTTY
|
|
12
|
+
|
|
13
|
+
function paint(text, code) {
|
|
14
|
+
return useColor ? `[${code}m${text}[0m` : text
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Prefix every line of a child's output with its labeled, colored tag. */
|
|
18
|
+
function pipePrefixed(stream, label, color) {
|
|
19
|
+
let buffer = ''
|
|
20
|
+
const tag = paint(`[${label}]`, color)
|
|
21
|
+
stream.on('data', (chunk) => {
|
|
22
|
+
buffer += chunk.toString()
|
|
23
|
+
const lines = buffer.split('\n')
|
|
24
|
+
buffer = lines.pop() ?? ''
|
|
25
|
+
for (const line of lines) stdout.write(`${tag} ${line}\n`)
|
|
26
|
+
})
|
|
27
|
+
stream.on('end', () => {
|
|
28
|
+
if (buffer.length) stdout.write(`${tag} ${buffer}\n`)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run a set of long-lived processes as one supervised group (`elytra dev`).
|
|
34
|
+
* Ctrl-C (SIGINT/SIGTERM) tears all of them down; if any child exits on its own,
|
|
35
|
+
* the rest are killed and the group resolves with that child's exit code.
|
|
36
|
+
*
|
|
37
|
+
* `procs`: [{ name, command, args, cwd }]. Returns the group's exit code.
|
|
38
|
+
*/
|
|
39
|
+
export function runGroup(procs) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const children = []
|
|
42
|
+
let shuttingDown = false
|
|
43
|
+
let resultCode = 0
|
|
44
|
+
|
|
45
|
+
const killAll = (signal) => {
|
|
46
|
+
for (const child of children) {
|
|
47
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
48
|
+
try {
|
|
49
|
+
child.kill(signal)
|
|
50
|
+
} catch {
|
|
51
|
+
/* already gone */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const shutdown = (code) => {
|
|
58
|
+
if (shuttingDown) return
|
|
59
|
+
shuttingDown = true
|
|
60
|
+
resultCode = code
|
|
61
|
+
killAll('SIGTERM')
|
|
62
|
+
// Hard stop if a child ignores SIGTERM.
|
|
63
|
+
const hard = setTimeout(() => killAll('SIGKILL'), 4000)
|
|
64
|
+
hard.unref?.()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
procs.forEach((proc, i) => {
|
|
68
|
+
const color = COLORS[i % COLORS.length]
|
|
69
|
+
stdout.write(`${paint(`[${proc.name}]`, color)} ${proc.command} ${proc.args.join(' ')}\n`)
|
|
70
|
+
const child = spawn(proc.command, proc.args, {
|
|
71
|
+
cwd: proc.cwd,
|
|
72
|
+
env: process.env,
|
|
73
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
|
+
})
|
|
75
|
+
children.push(child)
|
|
76
|
+
pipePrefixed(child.stdout, proc.name, color)
|
|
77
|
+
pipePrefixed(child.stderr, proc.name, color)
|
|
78
|
+
|
|
79
|
+
child.on('error', (err) => {
|
|
80
|
+
stdout.write(`${paint(`[${proc.name}]`, color)} failed to start: ${err.message}\n`)
|
|
81
|
+
shutdown(1)
|
|
82
|
+
})
|
|
83
|
+
child.on('exit', (code, signal) => {
|
|
84
|
+
if (!shuttingDown) {
|
|
85
|
+
// An unexpected exit takes the whole group down.
|
|
86
|
+
stdout.write(
|
|
87
|
+
`${paint(`[${proc.name}]`, color)} exited (${signal ?? code}); stopping the group.\n`,
|
|
88
|
+
)
|
|
89
|
+
shutdown(code ?? (signal ? 1 : 0))
|
|
90
|
+
}
|
|
91
|
+
if (children.every((c) => c.exitCode !== null || c.signalCode !== null)) {
|
|
92
|
+
resolve(resultCode)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const onSignal = () => shutdown(130)
|
|
98
|
+
process.once('SIGINT', onSignal)
|
|
99
|
+
process.once('SIGTERM', onSignal)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Run steps in order, failing the whole command on the first non-zero exit
|
|
105
|
+
* (`elytra build`). No silent partial builds. Returns the failing step's exit
|
|
106
|
+
* code, or 0 if all pass.
|
|
107
|
+
*/
|
|
108
|
+
export function runSequential(steps) {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
let i = 0
|
|
111
|
+
const next = () => {
|
|
112
|
+
if (i >= steps.length) return resolve(0)
|
|
113
|
+
const step = steps[i++]
|
|
114
|
+
stdout.write(`\n${paint(`▸ ${step.name}`, '36')}\n`)
|
|
115
|
+
const child = spawn(step.command, step.args, {
|
|
116
|
+
cwd: step.cwd,
|
|
117
|
+
env: process.env,
|
|
118
|
+
stdio: 'inherit',
|
|
119
|
+
})
|
|
120
|
+
child.on('error', (err) => {
|
|
121
|
+
stdout.write(`${step.name} failed to start: ${err.message}\n`)
|
|
122
|
+
resolve(1)
|
|
123
|
+
})
|
|
124
|
+
child.on('exit', (code, signal) => {
|
|
125
|
+
if (code === 0) return next()
|
|
126
|
+
stdout.write(`\n${step.name} failed (exit ${signal ?? code}). Stopping.\n`)
|
|
127
|
+
resolve(code ?? 1)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
next()
|
|
131
|
+
})
|
|
132
|
+
}
|