@geastack/cli 0.1.0
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 +674 -0
- package/README.md +161 -0
- package/bin/create-geastack.mjs +15 -0
- package/bin/gea.mjs +15 -0
- package/docs/ESP32-WAVESHARE-AMOLED-QUICKSTART.md +133 -0
- package/docs/NPX-COMMANDS.md +144 -0
- package/docs/SETUP.md +276 -0
- package/docs/SPEC.md +217 -0
- package/examples/catalog.json +668 -0
- package/package.json +41 -0
- package/src/args.mjs +78 -0
- package/src/board-catalog.mjs +224 -0
- package/src/context.mjs +110 -0
- package/src/create-geastack.mjs +430 -0
- package/src/errors.mjs +20 -0
- package/src/fs-utils.mjs +47 -0
- package/src/gea.mjs +444 -0
- package/src/manifest.mjs +171 -0
- package/src/prompts.mjs +60 -0
- package/src/run.mjs +33 -0
- package/src/serial-devices.mjs +143 -0
- package/src/setup-wizard.mjs +633 -0
- package/src/starter-catalog.mjs +191 -0
- package/src/toolchain.mjs +15 -0
- package/starters/bundled/counter/index.tsx +25 -0
- package/starters/bundled/counter/package.json +33 -0
- package/starters/bundled/counter/store.ts +7 -0
- package/starters/bundled/counter/styles.css +39 -0
- package/starters/catalog.json +22 -0
package/src/gea.mjs
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { flag, option, parseArgs } from './args.mjs'
|
|
4
|
+
import { createChildEnv, createContext } from './context.mjs'
|
|
5
|
+
import { ExitCode, fail } from './errors.mjs'
|
|
6
|
+
import { exists, readJson } from './fs-utils.mjs'
|
|
7
|
+
import {
|
|
8
|
+
assertTargetEnabled,
|
|
9
|
+
boardConfigPath,
|
|
10
|
+
assertValidApp,
|
|
11
|
+
discoverApps,
|
|
12
|
+
loadBoardConfig,
|
|
13
|
+
loadTargetMetadata,
|
|
14
|
+
resolveRequestedApp,
|
|
15
|
+
validateApp
|
|
16
|
+
} from './manifest.mjs'
|
|
17
|
+
import { runExternal } from './run.mjs'
|
|
18
|
+
import { runSetupWizard } from './setup-wizard.mjs'
|
|
19
|
+
import { commandVersion, nodeAtLeast } from './toolchain.mjs'
|
|
20
|
+
|
|
21
|
+
const version = '0.1.0'
|
|
22
|
+
|
|
23
|
+
export async function runGea(argv, io = {}) {
|
|
24
|
+
const parsed = parseArgs(argv)
|
|
25
|
+
const stdout = io.stdout || console.log
|
|
26
|
+
const stderr = io.stderr || console.error
|
|
27
|
+
const env = io.env || process.env
|
|
28
|
+
const cwd = io.cwd || process.cwd()
|
|
29
|
+
const stdin = io.stdin || process.stdin
|
|
30
|
+
const output = io.output || process.stdout
|
|
31
|
+
const prompt = io.prompt
|
|
32
|
+
const command = parsed.positionals[0]
|
|
33
|
+
|
|
34
|
+
if (flag(parsed, 'version')) {
|
|
35
|
+
stdout(version)
|
|
36
|
+
return 0
|
|
37
|
+
}
|
|
38
|
+
if (!command || command === 'help' || flag(parsed, 'help')) {
|
|
39
|
+
stdout(usage())
|
|
40
|
+
return 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ctx = createContext(parsed, env, cwd)
|
|
44
|
+
const rest = parsed.positionals.slice(1)
|
|
45
|
+
|
|
46
|
+
switch (command) {
|
|
47
|
+
case 'doctor':
|
|
48
|
+
return doctor(ctx, parsed, { stdout, env })
|
|
49
|
+
case 'dev':
|
|
50
|
+
return dev(ctx, parsed, rest, { stdout, env })
|
|
51
|
+
case 'build':
|
|
52
|
+
return build(ctx, parsed, rest, { stdout, env })
|
|
53
|
+
case 'setup':
|
|
54
|
+
return setup(ctx, parsed, { stdout, env, stdin, output, prompt })
|
|
55
|
+
case 'flash':
|
|
56
|
+
return flash(ctx, parsed, rest, { stdout, env })
|
|
57
|
+
case 'monitor':
|
|
58
|
+
return monitor(ctx, parsed, rest, { stdout, env })
|
|
59
|
+
case 'ota':
|
|
60
|
+
return ota(ctx, parsed, rest, { stdout, env })
|
|
61
|
+
case 'list':
|
|
62
|
+
return list(ctx, parsed, rest, { stdout })
|
|
63
|
+
case 'inspect':
|
|
64
|
+
return inspect(ctx, parsed, rest, { stdout })
|
|
65
|
+
default:
|
|
66
|
+
stderr(`Unknown command: ${command}`)
|
|
67
|
+
stdout(usage())
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function dev(ctx, parsed, rest, io) {
|
|
73
|
+
const app = resolveRequestedApp(ctx, parsed, rest)
|
|
74
|
+
assertValidApp(app)
|
|
75
|
+
const target = option(parsed, 'target', 'web')
|
|
76
|
+
assertTargetEnabled(ctx, app, target)
|
|
77
|
+
|
|
78
|
+
if (target !== 'web') {
|
|
79
|
+
io.stdout(`No live dev loop is registered for target '${target}' yet. Use 'gea flash --app ${app.id} --board <alias> --monitor'.`)
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
requirePath(ctx.scripts.webDev, 'web dev script')
|
|
84
|
+
const args = [ctx.scripts.webDev, app.id]
|
|
85
|
+
const port = option(parsed, 'port')
|
|
86
|
+
if (port) args.push('--port', String(port))
|
|
87
|
+
return runExternal('node', args, {
|
|
88
|
+
cwd: ctx.simulatorRoot,
|
|
89
|
+
env: createChildEnv(ctx, io.env),
|
|
90
|
+
dryRun: flag(parsed, 'dry-run'),
|
|
91
|
+
failureCode: ExitCode.buildFailed,
|
|
92
|
+
stdout: io.stdout
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function build(ctx, parsed, rest, io) {
|
|
97
|
+
const app = resolveRequestedApp(ctx, parsed, rest)
|
|
98
|
+
assertValidApp(app)
|
|
99
|
+
const board = option(parsed, 'board', '')
|
|
100
|
+
const target = option(parsed, 'target', board ? '' : 'web')
|
|
101
|
+
assertTargetEnabled(ctx, app, board || target)
|
|
102
|
+
|
|
103
|
+
if (target === 'web' && !board) {
|
|
104
|
+
requirePath(ctx.scripts.webBuild, 'web build script')
|
|
105
|
+
return runExternal(ctx.scripts.webBuild, [app.id], {
|
|
106
|
+
cwd: ctx.simulatorRoot,
|
|
107
|
+
env: createChildEnv(ctx, io.env),
|
|
108
|
+
dryRun: flag(parsed, 'dry-run'),
|
|
109
|
+
failureCode: ExitCode.buildFailed,
|
|
110
|
+
stdout: io.stdout
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (target === 'macos') {
|
|
115
|
+
requirePath(ctx.scripts.macosBuild, 'macOS build script')
|
|
116
|
+
const outputTag = option(parsed, 'output-tag')
|
|
117
|
+
if (outputTag !== undefined && (typeof outputTag !== 'string' || outputTag.length === 0)) {
|
|
118
|
+
fail('--output-tag requires a non-empty value.', ExitCode.usage)
|
|
119
|
+
}
|
|
120
|
+
const args = [app.id]
|
|
121
|
+
if (outputTag !== undefined) args.push('--output-tag', outputTag)
|
|
122
|
+
return runExternal(ctx.scripts.macosBuild, args, {
|
|
123
|
+
cwd: ctx.appleRoot,
|
|
124
|
+
env: createChildEnv(ctx, io.env),
|
|
125
|
+
dryRun: flag(parsed, 'dry-run'),
|
|
126
|
+
failureCode: ExitCode.buildFailed,
|
|
127
|
+
stdout: io.stdout
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (target === 'ios') {
|
|
132
|
+
requirePath(ctx.scripts.iosBuild, 'iOS build script')
|
|
133
|
+
const mode = option(parsed, 'mode', 'simulator')
|
|
134
|
+
return runExternal(ctx.scripts.iosBuild, [app.id, mode], {
|
|
135
|
+
cwd: ctx.appleRoot,
|
|
136
|
+
env: createChildEnv(ctx, io.env),
|
|
137
|
+
dryRun: flag(parsed, 'dry-run'),
|
|
138
|
+
failureCode: ExitCode.buildFailed,
|
|
139
|
+
stdout: io.stdout
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (target === 'android') {
|
|
144
|
+
return runAndroid(ctx, parsed, {
|
|
145
|
+
appId: app.id,
|
|
146
|
+
mode: option(parsed, 'mode', 'debug'),
|
|
147
|
+
failureCode: ExitCode.buildFailed,
|
|
148
|
+
stdout: io.stdout,
|
|
149
|
+
env: io.env
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return runBoard(ctx, 'build', parsed, {
|
|
154
|
+
app,
|
|
155
|
+
appId: app.id,
|
|
156
|
+
board,
|
|
157
|
+
target,
|
|
158
|
+
failureCode: ExitCode.buildFailed,
|
|
159
|
+
stdout: io.stdout,
|
|
160
|
+
env: io.env
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setup(ctx, parsed, io) {
|
|
165
|
+
const board = option(parsed, 'board', '')
|
|
166
|
+
const target = option(parsed, 'target', '')
|
|
167
|
+
if (!board && !target) {
|
|
168
|
+
return runSetupWizard(ctx, parsed, io)
|
|
169
|
+
}
|
|
170
|
+
return runBoard(ctx, 'setup', parsed, {
|
|
171
|
+
board,
|
|
172
|
+
target,
|
|
173
|
+
failureCode: ExitCode.buildFailed,
|
|
174
|
+
stdout: io.stdout,
|
|
175
|
+
env: io.env
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function flash(ctx, parsed, rest, io) {
|
|
180
|
+
const board = option(parsed, 'board', '')
|
|
181
|
+
const target = option(parsed, 'target', '')
|
|
182
|
+
if (!board && !target) fail('flash requires --board <alias> or --target <target>.', ExitCode.usage)
|
|
183
|
+
if (flag(parsed, 'bringup')) {
|
|
184
|
+
return runBoard(ctx, flag(parsed, 'monitor') ? 'flash-monitor' : 'flash', parsed, {
|
|
185
|
+
board,
|
|
186
|
+
target,
|
|
187
|
+
failureCode: ExitCode.deployFailed,
|
|
188
|
+
stdout: io.stdout,
|
|
189
|
+
env: io.env
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const app = resolveRequestedApp(ctx, parsed, rest)
|
|
194
|
+
assertValidApp(app)
|
|
195
|
+
assertTargetEnabled(ctx, app, board || target)
|
|
196
|
+
|
|
197
|
+
if (!board && target === 'android') {
|
|
198
|
+
return runAndroid(ctx, parsed, {
|
|
199
|
+
appId: app.id,
|
|
200
|
+
mode: 'device',
|
|
201
|
+
failureCode: ExitCode.deployFailed,
|
|
202
|
+
stdout: io.stdout,
|
|
203
|
+
env: io.env
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return runBoard(ctx, flag(parsed, 'monitor') ? 'flash-monitor' : 'flash', parsed, {
|
|
208
|
+
app,
|
|
209
|
+
appId: app.id,
|
|
210
|
+
board,
|
|
211
|
+
target,
|
|
212
|
+
failureCode: ExitCode.deployFailed,
|
|
213
|
+
stdout: io.stdout,
|
|
214
|
+
env: io.env
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function ota(ctx, parsed, rest, io) {
|
|
219
|
+
const board = option(parsed, 'board', '')
|
|
220
|
+
const target = option(parsed, 'target', '')
|
|
221
|
+
if (!board && !target) fail('ota requires --board <alias> or --target <target>.', ExitCode.usage)
|
|
222
|
+
|
|
223
|
+
const app = resolveRequestedApp(ctx, parsed, rest)
|
|
224
|
+
assertValidApp(app)
|
|
225
|
+
assertTargetEnabled(ctx, app, board || target)
|
|
226
|
+
|
|
227
|
+
const transport = option(parsed, 'transport', 'wifi')
|
|
228
|
+
if (transport !== 'wifi' && transport !== 'ble') {
|
|
229
|
+
fail("--transport must be 'wifi' or 'ble'.", ExitCode.usage)
|
|
230
|
+
}
|
|
231
|
+
return runBoard(ctx, transport === 'ble' ? 'ble-ota' : 'ota', parsed, {
|
|
232
|
+
app,
|
|
233
|
+
appId: app.id,
|
|
234
|
+
board,
|
|
235
|
+
target,
|
|
236
|
+
failureCode: ExitCode.deployFailed,
|
|
237
|
+
stdout: io.stdout,
|
|
238
|
+
env: io.env
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function monitor(ctx, parsed, rest, io) {
|
|
243
|
+
const board = option(parsed, 'board', '')
|
|
244
|
+
const target = option(parsed, 'target', '')
|
|
245
|
+
if (!board && !target) fail('monitor requires --board <alias> or --target <target>.', ExitCode.usage)
|
|
246
|
+
if (!board && target === 'android') {
|
|
247
|
+
return runAndroid(ctx, parsed, {
|
|
248
|
+
appId: 'css-3d-cube',
|
|
249
|
+
mode: 'monitor',
|
|
250
|
+
failureCode: ExitCode.deployFailed,
|
|
251
|
+
stdout: io.stdout,
|
|
252
|
+
env: io.env
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
return runBoard(ctx, 'monitor', parsed, {
|
|
256
|
+
board,
|
|
257
|
+
target,
|
|
258
|
+
failureCode: ExitCode.deployFailed,
|
|
259
|
+
stdout: io.stdout,
|
|
260
|
+
env: io.env
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function runBoard(ctx, action, parsed, opts) {
|
|
265
|
+
requirePath(ctx.scripts.board, 'board script')
|
|
266
|
+
const args = [action]
|
|
267
|
+
if (opts.board) args.push(`--board=${opts.board}`)
|
|
268
|
+
if (opts.target) args.push(`--target=${opts.target}`)
|
|
269
|
+
if (opts.appId) args.push(`--app=${opts.appId}`)
|
|
270
|
+
const residentApps = option(parsed, 'resident-apps')
|
|
271
|
+
if (residentApps) args.push(`--resident-apps=${residentApps}`)
|
|
272
|
+
const port = option(parsed, 'port')
|
|
273
|
+
if (port) args.push(String(port))
|
|
274
|
+
args.push(...parsed.passthrough)
|
|
275
|
+
const childEnv = createChildEnv(ctx, opts.env)
|
|
276
|
+
if (opts.app?.manifest?.ota?.ble === true) childEnv.GEA_EMBEDDED_BLE_OTA = '1'
|
|
277
|
+
return runExternal(ctx.scripts.board, args, {
|
|
278
|
+
cwd: ctx.targetsRoot,
|
|
279
|
+
env: childEnv,
|
|
280
|
+
dryRun: flag(parsed, 'dry-run'),
|
|
281
|
+
failureCode: opts.failureCode,
|
|
282
|
+
stdout: opts.stdout
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function runAndroid(ctx, parsed, opts) {
|
|
287
|
+
requirePath(ctx.scripts.androidBuild, 'Android build script')
|
|
288
|
+
return runExternal(ctx.scripts.androidBuild, [opts.appId, opts.mode, ...parsed.passthrough], {
|
|
289
|
+
cwd: ctx.androidRoot,
|
|
290
|
+
env: createChildEnv(ctx, opts.env),
|
|
291
|
+
dryRun: flag(parsed, 'dry-run'),
|
|
292
|
+
failureCode: opts.failureCode,
|
|
293
|
+
stdout: opts.stdout
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function list(ctx, parsed, rest, io) {
|
|
298
|
+
const subject = rest[0] || 'apps'
|
|
299
|
+
if (subject === 'apps') {
|
|
300
|
+
const target = option(parsed, 'target')
|
|
301
|
+
const apps = discoverApps(ctx)
|
|
302
|
+
.filter((app) => !target || app.targets?.[target] === true)
|
|
303
|
+
.map((app) => ({ id: app.id, name: app.name, targets: app.targets, root: app.root }))
|
|
304
|
+
if (flag(parsed, 'json')) io.stdout(JSON.stringify(apps, null, 2))
|
|
305
|
+
else for (const app of apps) io.stdout(app.id)
|
|
306
|
+
return 0
|
|
307
|
+
}
|
|
308
|
+
if (subject === 'targets') {
|
|
309
|
+
const targets = loadTargetMetadata(ctx)
|
|
310
|
+
if (flag(parsed, 'json')) io.stdout(JSON.stringify(targets, null, 2))
|
|
311
|
+
else for (const id of Object.keys(targets).sort()) io.stdout(id)
|
|
312
|
+
return 0
|
|
313
|
+
}
|
|
314
|
+
if (subject === 'boards') {
|
|
315
|
+
const boards = loadBoardConfig(ctx)
|
|
316
|
+
if (flag(parsed, 'json')) io.stdout(JSON.stringify(boards, null, 2))
|
|
317
|
+
else for (const id of Object.keys(boards).sort()) io.stdout(id)
|
|
318
|
+
return 0
|
|
319
|
+
}
|
|
320
|
+
fail(`Unknown list subject '${subject}'. Expected apps, targets, or boards.`, ExitCode.usage)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function inspect(ctx, parsed, rest, io) {
|
|
324
|
+
const app = resolveRequestedApp(ctx, parsed, rest)
|
|
325
|
+
assertValidApp(app)
|
|
326
|
+
const payload = {
|
|
327
|
+
id: app.id,
|
|
328
|
+
name: app.name,
|
|
329
|
+
root: app.root,
|
|
330
|
+
entry: app.entry,
|
|
331
|
+
runtime: app.runtime,
|
|
332
|
+
targets: app.targets,
|
|
333
|
+
icons: app.icons,
|
|
334
|
+
launcher: app.launcher
|
|
335
|
+
}
|
|
336
|
+
if (flag(parsed, 'json')) io.stdout(JSON.stringify(payload, null, 2))
|
|
337
|
+
else io.stdout(`${app.id}\t${app.name}\t${app.root}\t${app.entry}`)
|
|
338
|
+
return 0
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function doctor(ctx, parsed, io) {
|
|
342
|
+
const checks = []
|
|
343
|
+
addCheck(checks, '@geastack/targets', packageExists(ctx.targetsRoot), ctx.targetsRoot, true)
|
|
344
|
+
addCheck(checks, '@geastack/core', packageExists(ctx.corePackageDir), ctx.corePackageDir, true)
|
|
345
|
+
addCheck(checks, '@geastack/compiler', packageExists(ctx.compilerPackageDir), ctx.compilerPackageDir, true)
|
|
346
|
+
addCheck(checks, 'project root', exists(ctx.projectRoot), ctx.projectRoot, true)
|
|
347
|
+
addCheck(checks, 'web dev adapter', exists(ctx.scripts.webDev), ctx.scripts.webDev, false)
|
|
348
|
+
addCheck(checks, 'web build adapter', exists(ctx.scripts.webBuild), ctx.scripts.webBuild, false)
|
|
349
|
+
addCheck(checks, 'Android build adapter', exists(ctx.scripts.androidBuild), ctx.scripts.androidBuild, false)
|
|
350
|
+
addCheck(checks, 'board script', exists(ctx.scripts.board), ctx.scripts.board, true)
|
|
351
|
+
addCheck(checks, 'Node >= 20.19', nodeAtLeast(20, 19), process.version, true)
|
|
352
|
+
const npmVersion = commandVersion('npm', ['--version'], io.env)
|
|
353
|
+
const pythonCommand = commandVersion('python3', ['--version'], io.env) ? 'python3' : 'python'
|
|
354
|
+
const pythonVersion = commandVersion(pythonCommand, ['--version'], io.env)
|
|
355
|
+
const idfVersion = commandVersion('idf.py', ['--version'], io.env)
|
|
356
|
+
const emccVersion = commandVersion('emcc', ['--version'], io.env)
|
|
357
|
+
const xcodeVersion = commandVersion('xcodebuild', ['-version'], io.env)
|
|
358
|
+
const adbVersion = commandVersion('adb', ['version'], io.env)
|
|
359
|
+
const javacVersion = commandVersion('javac', ['--version'], io.env)
|
|
360
|
+
const androidSdk = io.env.ANDROID_HOME || io.env.ANDROID_SDK_ROOT || path.join(process.env.HOME || '', 'Library', 'Android', 'sdk')
|
|
361
|
+
addCheck(checks, 'npm', Boolean(npmVersion), npmVersion, false)
|
|
362
|
+
addCheck(checks, 'Python', Boolean(pythonVersion), pythonVersion, false)
|
|
363
|
+
addCheck(checks, 'ESP-IDF', Boolean(idfVersion) || Boolean(io.env.IDF_PATH), io.env.IDF_PATH || idfVersion, false)
|
|
364
|
+
addCheck(checks, 'Emscripten', Boolean(emccVersion), emccVersion.split('\n')[0], false)
|
|
365
|
+
addCheck(checks, 'Xcode', Boolean(xcodeVersion), xcodeVersion.split('\n')[0], false)
|
|
366
|
+
addCheck(checks, 'Android SDK', exists(androidSdk), androidSdk, false)
|
|
367
|
+
addCheck(checks, 'adb', Boolean(adbVersion), adbVersion.split('\n')[0], false)
|
|
368
|
+
addCheck(checks, 'javac', Boolean(javacVersion), javacVersion.split('\n')[0], false)
|
|
369
|
+
|
|
370
|
+
const boardFile = boardConfigPath(ctx)
|
|
371
|
+
try {
|
|
372
|
+
if (exists(boardFile)) readJson(boardFile)
|
|
373
|
+
addCheck(checks, 'boards.json', exists(boardFile), exists(boardFile) ? boardFile : 'not configured', false)
|
|
374
|
+
} catch (error) {
|
|
375
|
+
addCheck(checks, 'boards.json', false, error.message, true)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const apps = discoverApps(ctx)
|
|
379
|
+
addCheck(checks, 'app catalog', apps.length > 0, `${apps.length} app(s)`, true)
|
|
380
|
+
|
|
381
|
+
const currentApp = (() => {
|
|
382
|
+
try {
|
|
383
|
+
return resolveRequestedApp(ctx, parsed, [])
|
|
384
|
+
} catch {
|
|
385
|
+
return null
|
|
386
|
+
}
|
|
387
|
+
})()
|
|
388
|
+
if (currentApp) {
|
|
389
|
+
const errors = validateApp(currentApp)
|
|
390
|
+
addCheck(checks, `app manifest (${currentApp.id})`, errors.length === 0, errors.length === 0 ? currentApp.root : errors.join('; '), true)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const failedRequired = checks.filter((check) => check.required && !check.ok)
|
|
394
|
+
const failedOptional = checks.filter((check) => !check.required && !check.ok)
|
|
395
|
+
|
|
396
|
+
if (flag(parsed, 'json')) {
|
|
397
|
+
io.stdout(JSON.stringify({ ok: failedRequired.length === 0, checks }, null, 2))
|
|
398
|
+
} else {
|
|
399
|
+
for (const check of checks) {
|
|
400
|
+
const marker = check.ok ? '[ok]' : check.required ? '[fail]' : '[warn]'
|
|
401
|
+
io.stdout(`${marker} ${check.name}: ${check.detail}`)
|
|
402
|
+
}
|
|
403
|
+
if (failedRequired.length > 0 || failedOptional.length > 0) {
|
|
404
|
+
io.stdout('Setup guide: cli/docs/SETUP.md')
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (failedRequired.length > 0) return ExitCode.missingDependency
|
|
409
|
+
if (failedOptional.length > 0 && flag(parsed, 'strict')) return ExitCode.missingDependency
|
|
410
|
+
return 0
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function addCheck(checks, name, ok, detail, required) {
|
|
414
|
+
checks.push({ name, ok: Boolean(ok), detail: detail || (ok ? 'ok' : 'missing'), required: Boolean(required) })
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function packageExists(packageDir) {
|
|
418
|
+
return Boolean(packageDir) && exists(path.join(packageDir, 'package.json'))
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function requirePath(filePath, label) {
|
|
422
|
+
if (!exists(filePath)) fail(`Missing ${label}: ${filePath}`, ExitCode.missingDependency)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function usage() {
|
|
426
|
+
return `Usage:
|
|
427
|
+
gea doctor [--strict] [--json]
|
|
428
|
+
gea setup --board <alias>
|
|
429
|
+
gea dev [app] [--target web] [--port 5181]
|
|
430
|
+
gea build [app] [--target web|macos|ios|android|<target>] [--board <alias>] [--output-tag <tag>]
|
|
431
|
+
gea flash [app] --board <alias> [--monitor] [--port auto]
|
|
432
|
+
gea flash [app] --target android
|
|
433
|
+
gea flash --bringup --board <alias> [--monitor] [--port auto]
|
|
434
|
+
gea monitor --board <alias>
|
|
435
|
+
gea monitor --target android
|
|
436
|
+
gea ota [app] --board <alias> [--transport wifi|ble]
|
|
437
|
+
gea list [apps|targets|boards] [--json]
|
|
438
|
+
gea inspect [app] [--json]
|
|
439
|
+
|
|
440
|
+
Global options:
|
|
441
|
+
--boards-config <file>
|
|
442
|
+
--dry-run
|
|
443
|
+
`
|
|
444
|
+
}
|
package/src/manifest.mjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { CliError, ExitCode, fail } from './errors.mjs'
|
|
4
|
+
import { exists, findUp, isDirectory, listDirectories, readJson } from './fs-utils.mjs'
|
|
5
|
+
|
|
6
|
+
export function discoverApps(ctx) {
|
|
7
|
+
const roots = [ctx.projectRoot]
|
|
8
|
+
const seen = new Set()
|
|
9
|
+
const apps = []
|
|
10
|
+
for (const root of roots) {
|
|
11
|
+
for (const app of discoverAppsInRoot(root)) {
|
|
12
|
+
const key = app.root
|
|
13
|
+
if (seen.has(key)) continue
|
|
14
|
+
seen.add(key)
|
|
15
|
+
apps.push(app)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return apps.sort((a, b) => a.id.localeCompare(b.id))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function discoverAppsInRoot(root) {
|
|
22
|
+
if (!isDirectory(root)) return []
|
|
23
|
+
const candidates = [
|
|
24
|
+
path.join(root, 'package.json'),
|
|
25
|
+
...listDirectories(path.join(root, 'apps')).map((dir) => path.join(dir, 'package.json')),
|
|
26
|
+
...listDirectories(path.join(root, 'examples')).map((dir) => path.join(dir, 'package.json'))
|
|
27
|
+
]
|
|
28
|
+
return candidates.flatMap((manifestPath) => {
|
|
29
|
+
if (!exists(manifestPath)) return []
|
|
30
|
+
try {
|
|
31
|
+
const packageJson = readJson(manifestPath)
|
|
32
|
+
if (!packageJson.gea) return []
|
|
33
|
+
return [normalizeApp(path.dirname(manifestPath), packageJson)]
|
|
34
|
+
} catch {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveRequestedApp(ctx, parsed, positionals) {
|
|
41
|
+
const requested = parsed.options.app || positionals[0]
|
|
42
|
+
if (requested) {
|
|
43
|
+
const app = findAppById(ctx, String(Array.isArray(requested) ? requested.at(-1) : requested))
|
|
44
|
+
if (!app) fail(`Could not find Gea app '${requested}' in ${ctx.projectRoot}.`, ExitCode.usage)
|
|
45
|
+
return app
|
|
46
|
+
}
|
|
47
|
+
const current = findCurrentApp(ctx.cwd)
|
|
48
|
+
if (current) return current
|
|
49
|
+
fail('No app selected. Pass --app <id>, pass an app id, or run inside a Gea app folder.', ExitCode.usage)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function findAppById(ctx, id) {
|
|
53
|
+
return discoverApps(ctx).find((app) => app.id === id) || null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function findCurrentApp(cwd) {
|
|
57
|
+
const packageDir = findUp(cwd, (dir) => exists(path.join(dir, 'package.json')) && hasGeaManifest(path.join(dir, 'package.json')))
|
|
58
|
+
if (!packageDir) return null
|
|
59
|
+
return normalizeApp(packageDir, readJson(path.join(packageDir, 'package.json')))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function validateApp(app) {
|
|
63
|
+
const errors = []
|
|
64
|
+
if (!app.id) errors.push('gea.id is required')
|
|
65
|
+
if (!app.entry) errors.push('gea.entry is required')
|
|
66
|
+
if (app.entry && !exists(path.join(app.root, app.entry))) errors.push(`gea.entry does not exist: ${app.entry}`)
|
|
67
|
+
if (!app.runtime) errors.push('gea.runtime is required or must default to gea')
|
|
68
|
+
if (!app.targets || typeof app.targets !== 'object' || Array.isArray(app.targets)) errors.push('gea.targets must be an object')
|
|
69
|
+
return errors
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function assertValidApp(app) {
|
|
73
|
+
const errors = validateApp(app)
|
|
74
|
+
if (errors.length > 0) {
|
|
75
|
+
throw new CliError(`ERROR: Invalid Gea app '${app.id || app.root}':\n${errors.map((line) => ` - ${line}`).join('\n')}`, ExitCode.usage)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function targetEnabledForApp(ctx, app, targetOrPlatform) {
|
|
80
|
+
if (!targetOrPlatform) return true
|
|
81
|
+
const platforms = appPlatformsForTarget(ctx, targetOrPlatform)
|
|
82
|
+
return platforms.some((platform) => app.targets?.[platform] === true)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function assertTargetEnabled(ctx, app, targetOrPlatform) {
|
|
86
|
+
if (!targetEnabledForApp(ctx, app, targetOrPlatform)) {
|
|
87
|
+
const platform = appPlatformForTarget(ctx, targetOrPlatform) || targetOrPlatform
|
|
88
|
+
fail(`App '${app.id}' does not enable target '${platform}'.`, ExitCode.targetUnavailable)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function appPlatformForTarget(ctx, targetOrBoard) {
|
|
93
|
+
return appPlatformsForTarget(ctx, targetOrBoard)[0] || ''
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function appPlatformsForTarget(ctx, targetOrBoard) {
|
|
97
|
+
if (!targetOrBoard) return []
|
|
98
|
+
if (['web', 'esp32', 'rp2350', 'geaos', 'macos', 'ios', 'android'].includes(targetOrBoard)) return [targetOrBoard]
|
|
99
|
+
const targets = loadTargetMetadata(ctx)
|
|
100
|
+
if (targets[targetOrBoard]?.appPlatform) return targetAppPlatforms(targets[targetOrBoard])
|
|
101
|
+
const boards = loadBoardConfig(ctx)
|
|
102
|
+
const board = boards[targetOrBoard]
|
|
103
|
+
const boardTarget = board?.target
|
|
104
|
+
if (board?.appPlatform) {
|
|
105
|
+
return uniquePlatforms([
|
|
106
|
+
board.appPlatform,
|
|
107
|
+
...(Array.isArray(board.compatibleAppPlatforms) ? board.compatibleAppPlatforms : []),
|
|
108
|
+
...(boardTarget && targets[boardTarget] ? targetAppPlatforms(targets[boardTarget]).slice(1) : [])
|
|
109
|
+
])
|
|
110
|
+
}
|
|
111
|
+
if (boardTarget && targets[boardTarget]?.appPlatform) return targetAppPlatforms(targets[boardTarget])
|
|
112
|
+
return [targetOrBoard]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function loadTargetMetadata(ctx) {
|
|
116
|
+
const file = path.join(ctx.targetsRoot, 'scripts', 'boards', 'targets.json')
|
|
117
|
+
return exists(file) ? readJson(file) : {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function loadBoardConfig(ctx) {
|
|
121
|
+
const file = boardConfigPath(ctx)
|
|
122
|
+
return exists(file) ? readJson(file) : {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function boardConfigPath(ctx) {
|
|
126
|
+
if (ctx.boardsConfig) return ctx.boardsConfig
|
|
127
|
+
if (ctx.projectBoardsConfig && exists(ctx.projectBoardsConfig)) return ctx.projectBoardsConfig
|
|
128
|
+
return path.join(ctx.targetsRoot, 'boards.json')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasGeaManifest(packagePath) {
|
|
132
|
+
try {
|
|
133
|
+
return Boolean(readJson(packagePath).gea)
|
|
134
|
+
} catch {
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function targetAppPlatforms(targetInfo) {
|
|
140
|
+
return uniquePlatforms([
|
|
141
|
+
targetInfo.appPlatform,
|
|
142
|
+
...(Array.isArray(targetInfo.compatibleAppPlatforms) ? targetInfo.compatibleAppPlatforms : [])
|
|
143
|
+
])
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function uniquePlatforms(values) {
|
|
147
|
+
const out = []
|
|
148
|
+
for (const value of values) {
|
|
149
|
+
if (!value || out.includes(value)) continue
|
|
150
|
+
out.push(value)
|
|
151
|
+
}
|
|
152
|
+
return out
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeApp(root, packageJson) {
|
|
156
|
+
const gea = packageJson.gea || {}
|
|
157
|
+
return {
|
|
158
|
+
id: String(gea.id || ''),
|
|
159
|
+
name: String(gea.name || packageJson.name || gea.id || ''),
|
|
160
|
+
packageName: packageJson.name || '',
|
|
161
|
+
version: packageJson.version || '',
|
|
162
|
+
root: path.resolve(root),
|
|
163
|
+
entry: gea.entry || 'index.tsx',
|
|
164
|
+
runtime: gea.runtime || 'gea',
|
|
165
|
+
targets: gea.targets || {},
|
|
166
|
+
icons: gea.icons || {},
|
|
167
|
+
launcher: gea.launcher || {},
|
|
168
|
+
manifest: gea,
|
|
169
|
+
packageJson
|
|
170
|
+
}
|
|
171
|
+
}
|
package/src/prompts.mjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import readline from 'node:readline/promises'
|
|
2
|
+
|
|
3
|
+
export function canPrompt(io = {}) {
|
|
4
|
+
return Boolean(io.prompt || io.stdin?.isTTY || (!io.stdin && process.stdin.isTTY))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createPrompt(io = {}) {
|
|
8
|
+
if (io.prompt) return io.prompt
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: io.stdin || process.stdin,
|
|
11
|
+
output: io.output || process.stdout
|
|
12
|
+
})
|
|
13
|
+
return {
|
|
14
|
+
async ask(question) {
|
|
15
|
+
return rl.question(question)
|
|
16
|
+
},
|
|
17
|
+
async close() {
|
|
18
|
+
rl.close()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function ask(prompt, { message, defaultValue = '', validate = () => '' }) {
|
|
24
|
+
while (true) {
|
|
25
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : ''
|
|
26
|
+
const raw = await prompt.ask(`? ${message}${suffix}: `)
|
|
27
|
+
const value = String(raw || defaultValue).trim()
|
|
28
|
+
const error = validate(value)
|
|
29
|
+
if (!error) return value
|
|
30
|
+
if (prompt.write) prompt.write(`Invalid value: ${error}`)
|
|
31
|
+
else console.error(`Invalid value: ${error}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function choose(prompt, { message, choices, defaultValue }) {
|
|
36
|
+
const labels = choices.map((choice, index) => formatChoice(choice, index)).join('\n')
|
|
37
|
+
const defaultIndex = Math.max(0, choices.findIndex((choice) => choice.value === defaultValue))
|
|
38
|
+
while (true) {
|
|
39
|
+
const raw = await prompt.ask(`? ${message}\n${labels}\nChoose [${defaultIndex + 1}]: `)
|
|
40
|
+
const value = String(raw || String(defaultIndex + 1)).trim()
|
|
41
|
+
const byNumber = choices[Number.parseInt(value, 10) - 1]
|
|
42
|
+
if (byNumber) return byNumber.value
|
|
43
|
+
const byValue = choices.find((choice) => choice.value === value || choice.label === value)
|
|
44
|
+
if (byValue) return byValue.value
|
|
45
|
+
if (prompt.write) prompt.write(`Invalid choice: ${value}`)
|
|
46
|
+
else console.error(`Invalid choice: ${value}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function confirm(prompt, { message, defaultValue = false }) {
|
|
51
|
+
const hint = defaultValue ? 'Y/n' : 'y/N'
|
|
52
|
+
const raw = await prompt.ask(`? ${message} [${hint}]: `)
|
|
53
|
+
const value = String(raw || (defaultValue ? 'y' : 'n')).trim().toLowerCase()
|
|
54
|
+
return ['y', 'yes', 'true', '1'].includes(value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatChoice(choice, index) {
|
|
58
|
+
const description = choice.description ? `\n ${choice.description}` : ''
|
|
59
|
+
return `${index + 1}. ${choice.label}${description}`
|
|
60
|
+
}
|