@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/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
+ }
@@ -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
+ }
@@ -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
+ }