@geastack/cli 0.1.51 → 0.1.53

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 CHANGED
@@ -1,28 +1,20 @@
1
- import path from 'node:path'
2
1
  import fs from 'node:fs'
3
2
 
4
3
  import { flag, option, parseArgs } from './args.mjs'
5
- import { runCreateGeastack } from './create-geastack.mjs'
6
4
  import { runChips } from './chips.mjs'
7
- import { createChildEnv, createContext } from './context.mjs'
5
+ import { appsCommand, boardsCommand, heapReportCommand, targetsCommand } from './commands/apps.mjs'
6
+ import { buildCommand, cleanCommand, devctlCommand, flashCommand, geaosDeviceCommand, logsCommand, monitorCommand, otaCommand, screenshotCommand } from './commands/board.mjs'
7
+ import { doctorCommand } from './commands/doctor.mjs'
8
+ import { createContext } from './context.mjs'
9
+ import { runCreateGeastack } from './create-geastack.mjs'
8
10
  import { ExitCode, fail } from './errors.mjs'
9
- import { exists, readJson } from './fs-utils.mjs'
10
- import {
11
- assertTargetEnabled,
12
- boardConfigPath,
13
- assertValidApp,
14
- discoverApps,
15
- loadBoardConfig,
16
- loadTargetMetadata,
17
- resolveRequestedApp,
18
- validateApp
19
- } from './manifest.mjs'
20
- import { runExternal } from './run.mjs'
11
+ import { knownPlatforms } from './manifest.mjs'
21
12
  import { runSetupWizard } from './setup-wizard.mjs'
22
- import { commandVersion, nodeAtLeast } from './toolchain.mjs'
23
13
 
24
14
  const version = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
25
15
 
16
+ const geaosDeviceActions = new Set(['bringup', 'probe', 'shell', 'push', 'record', 'tap', 'drag', 'down', 'move', 'up', 'flash-kernel'])
17
+
26
18
  export async function runGea(argv, io = {}) {
27
19
  const parsed = parseArgs(argv)
28
20
  const stdout = io.stdout || console.log
@@ -50,438 +42,105 @@ export async function runGea(argv, io = {}) {
50
42
 
51
43
  const ctx = createContext(parsed, env, cwd)
52
44
  const rest = parsed.positionals.slice(1)
45
+ const options = { stdout, stderr, env, stdin, output, prompt }
46
+
47
+ // A platform name in --target (web, macos, ...) is not a board.
48
+ const target = option(parsed, 'target', '')
49
+ if (target && knownPlatforms.includes(target) && ['build', 'flash', 'run', 'monitor', 'ota'].includes(command)) {
50
+ if (target === 'esp32' || target === 'rp2350' || target === 'geaos') {
51
+ fail(`--target ${target} names a platform; pass --board <alias> (gea boards list) or --target <target id> (gea targets list).`, ExitCode.usage)
52
+ }
53
+ fail(`The ${target} build is not driven by gea; run its build script in the ${target} target project.`, ExitCode.usage)
54
+ }
53
55
 
54
56
  switch (command) {
55
57
  case 'doctor':
56
- return doctor(ctx, parsed, { stdout, env })
57
- case 'dev':
58
- return dev(ctx, parsed, rest, { stdout, env })
59
- case 'build':
60
- return build(ctx, parsed, rest, { stdout, env })
58
+ return doctorCommand(ctx, parsed, rest, options)
61
59
  case 'setup':
62
- return setup(ctx, parsed, { stdout, env, stdin, output, prompt })
60
+ return option(parsed, 'board') || option(parsed, 'target')
61
+ ? buildCommand(ctx, { ...parsed, options: { ...parsed.options, 'configure-only': true } }, rest, options)
62
+ : runSetupWizard(ctx, parsed, options)
63
63
  case 'chips':
64
- return runChips(ctx, parsed, rest, { stdout, env, stdin, output, prompt })
64
+ return runChips(ctx, parsed, rest, options)
65
+ case 'build':
66
+ return buildCommand(ctx, parsed, rest, options)
67
+ case 'clean':
68
+ return cleanCommand(ctx, parsed, rest, options)
65
69
  case 'flash':
66
- return flash(ctx, parsed, rest, { stdout, env })
70
+ return flashCommand(ctx, parsed, rest, options, { monitor: flag(parsed, 'monitor') })
71
+ case 'run':
72
+ return flashCommand(ctx, parsed, rest, options, { monitor: true })
67
73
  case 'monitor':
68
- return monitor(ctx, parsed, rest, { stdout, env })
74
+ return monitorCommand(ctx, parsed, rest, options)
75
+ case 'logs':
76
+ case 'tail':
77
+ return logsCommand(ctx, parsed, rest, options)
69
78
  case 'screenshot':
70
- return screenshot(ctx, parsed, rest, { stdout, env })
79
+ return screenshotCommand(ctx, parsed, rest, options)
71
80
  case 'ota':
72
- return ota(ctx, parsed, rest, { stdout, env })
81
+ return otaCommand(ctx, parsed, rest, options)
82
+ case 'devctl':
83
+ return devctlCommand(ctx, parsed, rest, options)
84
+ case 'apps':
85
+ return appsCommand(ctx, parsed, rest, options)
86
+ case 'boards':
87
+ return boardsCommand(ctx, parsed, rest, options)
88
+ case 'targets':
89
+ return targetsCommand(ctx, parsed, rest, options)
90
+ case 'heap-report':
91
+ return heapReportCommand(ctx, parsed, rest, options)
73
92
  case 'list':
74
- return list(ctx, parsed, rest, { stdout })
93
+ return legacyList(ctx, parsed, rest, options)
75
94
  case 'inspect':
76
- return inspect(ctx, parsed, rest, { stdout })
95
+ return appsCommand(ctx, parsed, ['inspect', ...rest], options)
77
96
  default:
97
+ if (geaosDeviceActions.has(command)) return geaosDeviceCommand(ctx, parsed, [command, ...rest], options)
78
98
  stderr(`Unknown command: ${command}`)
79
99
  stdout(usage())
80
100
  return 1
81
101
  }
82
102
  }
83
103
 
84
- function dev(ctx, parsed, rest, io) {
85
- const app = resolveRequestedApp(ctx, parsed, rest)
86
- assertValidApp(app)
87
- const target = option(parsed, 'target', 'web')
88
- assertTargetEnabled(ctx, app, target)
89
-
90
- if (target !== 'web') {
91
- io.stdout(`No live dev loop is registered for target '${target}' yet. Use 'gea flash --app ${app.id} --board <alias> --monitor'.`)
92
- return 0
93
- }
94
-
95
- requirePath(ctx.scripts.webDev, 'web dev script')
96
- const args = [ctx.scripts.webDev, app.id]
97
- const port = option(parsed, 'port')
98
- if (port) args.push('--port', String(port))
99
- return runExternal('node', args, {
100
- cwd: ctx.simulatorRoot,
101
- env: createChildEnv(ctx, io.env),
102
- dryRun: flag(parsed, 'dry-run'),
103
- failureCode: ExitCode.buildFailed,
104
- stdout: io.stdout
105
- })
106
- }
107
-
108
- function build(ctx, parsed, rest, io) {
109
- const app = resolveRequestedApp(ctx, parsed, rest)
110
- assertValidApp(app)
111
- const board = option(parsed, 'board', '')
112
- const target = option(parsed, 'target', board ? '' : 'web')
113
- assertTargetEnabled(ctx, app, board || target)
114
-
115
- if (target === 'web' && !board) {
116
- requirePath(ctx.scripts.webBuild, 'web build script')
117
- return runExternal(ctx.scripts.webBuild, [app.id], {
118
- cwd: ctx.simulatorRoot,
119
- env: createChildEnv(ctx, io.env),
120
- dryRun: flag(parsed, 'dry-run'),
121
- failureCode: ExitCode.buildFailed,
122
- stdout: io.stdout
123
- })
124
- }
125
-
126
- if (target === 'macos') {
127
- requirePath(ctx.scripts.macosBuild, 'macOS build script')
128
- const outputTag = option(parsed, 'output-tag')
129
- if (outputTag !== undefined && (typeof outputTag !== 'string' || outputTag.length === 0)) {
130
- fail('--output-tag requires a non-empty value.', ExitCode.usage)
131
- }
132
- const args = [app.id]
133
- if (outputTag !== undefined) args.push('--output-tag', outputTag)
134
- return runExternal(ctx.scripts.macosBuild, args, {
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 === 'ios') {
144
- requirePath(ctx.scripts.iosBuild, 'iOS build script')
145
- const mode = option(parsed, 'mode', 'simulator')
146
- return runExternal(ctx.scripts.iosBuild, [app.id, mode], {
147
- cwd: ctx.appleRoot,
148
- env: createChildEnv(ctx, io.env),
149
- dryRun: flag(parsed, 'dry-run'),
150
- failureCode: ExitCode.buildFailed,
151
- stdout: io.stdout
152
- })
153
- }
154
-
155
- if (target === 'android') {
156
- return runAndroid(ctx, parsed, {
157
- appId: app.id,
158
- mode: option(parsed, 'mode', 'debug'),
159
- failureCode: ExitCode.buildFailed,
160
- stdout: io.stdout,
161
- env: io.env
162
- })
163
- }
164
-
165
- return runBoard(ctx, 'build', parsed, {
166
- app,
167
- appId: app.id,
168
- board,
169
- target,
170
- failureCode: ExitCode.buildFailed,
171
- stdout: io.stdout,
172
- env: io.env
173
- })
174
- }
175
-
176
- function setup(ctx, parsed, io) {
177
- const board = option(parsed, 'board', '')
178
- const target = option(parsed, 'target', '')
179
- if (!board && !target) {
180
- return runSetupWizard(ctx, parsed, io)
181
- }
182
- return runBoard(ctx, 'setup', parsed, {
183
- board,
184
- target,
185
- failureCode: ExitCode.buildFailed,
186
- stdout: io.stdout,
187
- env: io.env
188
- })
189
- }
190
-
191
- function flash(ctx, parsed, rest, io) {
192
- const board = option(parsed, 'board', '')
193
- const target = option(parsed, 'target', '')
194
- if (!board && !target) fail('flash requires --board <alias> or --target <target>.', ExitCode.usage)
195
- if (flag(parsed, 'bringup')) {
196
- return runBoard(ctx, flag(parsed, 'monitor') ? 'flash-monitor' : 'flash', parsed, {
197
- board,
198
- target,
199
- failureCode: ExitCode.deployFailed,
200
- stdout: io.stdout,
201
- env: io.env
202
- })
203
- }
204
-
205
- const app = resolveRequestedApp(ctx, parsed, rest)
206
- assertValidApp(app)
207
- assertTargetEnabled(ctx, app, board || target)
208
-
209
- if (!board && target === 'android') {
210
- return runAndroid(ctx, parsed, {
211
- appId: app.id,
212
- mode: 'device',
213
- failureCode: ExitCode.deployFailed,
214
- stdout: io.stdout,
215
- env: io.env
216
- })
217
- }
218
-
219
- return runBoard(ctx, flag(parsed, 'monitor') ? 'flash-monitor' : 'flash', parsed, {
220
- app,
221
- appId: app.id,
222
- board,
223
- target,
224
- failureCode: ExitCode.deployFailed,
225
- stdout: io.stdout,
226
- env: io.env
227
- })
228
- }
229
-
230
- function ota(ctx, parsed, rest, io) {
231
- const board = option(parsed, 'board', '')
232
- const target = option(parsed, 'target', '')
233
- if (!board && !target) fail('ota requires --board <alias> or --target <target>.', ExitCode.usage)
234
-
235
- const app = resolveRequestedApp(ctx, parsed, rest)
236
- assertValidApp(app)
237
- assertTargetEnabled(ctx, app, board || target)
238
-
239
- const transport = option(parsed, 'transport', 'wifi')
240
- if (transport !== 'wifi' && transport !== 'ble') {
241
- fail("--transport must be 'wifi' or 'ble'.", ExitCode.usage)
242
- }
243
- return runBoard(ctx, transport === 'ble' ? 'ble-ota' : 'ota', parsed, {
244
- app,
245
- appId: app.id,
246
- board,
247
- target,
248
- failureCode: ExitCode.deployFailed,
249
- stdout: io.stdout,
250
- env: io.env
251
- })
252
- }
253
-
254
- function monitor(ctx, parsed, rest, io) {
255
- const board = option(parsed, 'board', '')
256
- const target = option(parsed, 'target', '')
257
- if (!board && !target) fail('monitor requires --board <alias> or --target <target>.', ExitCode.usage)
258
- if (!board && target === 'android') {
259
- return runAndroid(ctx, parsed, {
260
- appId: 'css-3d-cube',
261
- mode: 'monitor',
262
- failureCode: ExitCode.deployFailed,
263
- stdout: io.stdout,
264
- env: io.env
265
- })
266
- }
267
- return runBoard(ctx, 'monitor', parsed, {
268
- board,
269
- target,
270
- failureCode: ExitCode.deployFailed,
271
- stdout: io.stdout,
272
- env: io.env
273
- })
274
- }
275
-
276
- function screenshot(ctx, parsed, rest, io) {
277
- const board = option(parsed, 'board', '')
278
- const target = option(parsed, 'target', '')
279
- if (!board && !target) fail('screenshot requires --board <alias> or --target <target>.', ExitCode.usage)
280
-
281
- const output = path.resolve(ctx.cwd, rest[0] || 'screenshot.png')
282
- const extraArgs = [output]
283
- const timeout = option(parsed, 'timeout')
284
- if (timeout !== undefined) extraArgs.push(`--timeout=${timeout}`)
285
- if (flag(parsed, 'legacy')) extraArgs.push('--legacy')
286
-
287
- return runBoard(ctx, 'screenshot', parsed, {
288
- board,
289
- target,
290
- extraArgs,
291
- namedPort: true,
292
- failureCode: ExitCode.deployFailed,
293
- stdout: io.stdout,
294
- env: io.env
295
- })
296
- }
297
-
298
- function runBoard(ctx, action, parsed, opts) {
299
- requirePath(ctx.scripts.board, 'board script')
300
- const args = [action]
301
- if (opts.board) args.push(`--board=${opts.board}`)
302
- if (opts.target) args.push(`--target=${opts.target}`)
303
- if (opts.appId) args.push(`--app=${opts.appId}`)
304
- const port = option(parsed, 'port')
305
- if (port) args.push(opts.namedPort ? `--port=${port}` : String(port))
306
- if (flag(parsed, 'manual-boot')) args.push('--manual-boot')
307
- if (option(parsed, 'reset') === false) args.push('--no-reset')
308
- const flashBaud = option(parsed, 'flash-baud')
309
- if (flashBaud !== undefined) args.push(`--flash-baud=${flashBaud}`)
310
- if (opts.extraArgs) args.push(...opts.extraArgs)
311
- args.push(...parsed.passthrough)
312
- const childEnv = createChildEnv(ctx, opts.env)
313
- if (opts.app?.manifest?.ota?.ble === true) childEnv.GEA_EMBEDDED_BLE_OTA = '1'
314
- return runExternal(ctx.scripts.board, args, {
315
- cwd: ctx.targetsRoot,
316
- env: childEnv,
317
- dryRun: flag(parsed, 'dry-run'),
318
- failureCode: opts.failureCode,
319
- stdout: opts.stdout
320
- })
321
- }
322
-
323
- function runAndroid(ctx, parsed, opts) {
324
- requirePath(ctx.scripts.androidBuild, 'Android build script')
325
- return runExternal(ctx.scripts.androidBuild, [opts.appId, opts.mode, ...parsed.passthrough], {
326
- cwd: ctx.androidRoot,
327
- env: createChildEnv(ctx, opts.env),
328
- dryRun: flag(parsed, 'dry-run'),
329
- failureCode: opts.failureCode,
330
- stdout: opts.stdout
331
- })
332
- }
333
-
334
- function list(ctx, parsed, rest, io) {
104
+ function legacyList(ctx, parsed, rest, options) {
335
105
  const subject = rest[0] || 'apps'
336
- if (subject === 'apps') {
337
- const target = option(parsed, 'target')
338
- const apps = discoverApps(ctx)
339
- .filter((app) => !target || app.targets?.[target] === true)
340
- .map((app) => ({ id: app.id, name: app.name, targets: app.targets, root: app.root }))
341
- if (flag(parsed, 'json')) io.stdout(JSON.stringify(apps, null, 2))
342
- else for (const app of apps) io.stdout(app.id)
343
- return 0
344
- }
345
- if (subject === 'targets') {
346
- const targets = loadTargetMetadata(ctx)
347
- if (flag(parsed, 'json')) io.stdout(JSON.stringify(targets, null, 2))
348
- else for (const id of Object.keys(targets).sort()) io.stdout(id)
349
- return 0
350
- }
351
- if (subject === 'boards') {
352
- const boards = loadBoardConfig(ctx)
353
- if (flag(parsed, 'json')) io.stdout(JSON.stringify(boards, null, 2))
354
- else for (const id of Object.keys(boards).sort()) io.stdout(id)
355
- return 0
356
- }
106
+ if (subject === 'apps') return appsCommand(ctx, parsed, ['list'], options)
107
+ if (subject === 'targets') return targetsCommand(ctx, parsed, ['list'], options)
108
+ if (subject === 'boards') return boardsCommand(ctx, parsed, ['list'], options)
357
109
  fail(`Unknown list subject '${subject}'. Expected apps, targets, or boards.`, ExitCode.usage)
358
110
  }
359
111
 
360
- function inspect(ctx, parsed, rest, io) {
361
- const app = resolveRequestedApp(ctx, parsed, rest)
362
- assertValidApp(app)
363
- const payload = {
364
- id: app.id,
365
- name: app.name,
366
- root: app.root,
367
- entry: app.entry,
368
- runtime: app.runtime,
369
- targets: app.targets,
370
- icons: app.icons,
371
- launcher: app.launcher
372
- }
373
- if (flag(parsed, 'json')) io.stdout(JSON.stringify(payload, null, 2))
374
- else io.stdout(`${app.id}\t${app.name}\t${app.root}\t${app.entry}`)
375
- return 0
376
- }
377
-
378
- function doctor(ctx, parsed, io) {
379
- const checks = []
380
- addCheck(checks, '@geastack/targets', packageExists(ctx.targetsRoot), ctx.targetsRoot, true)
381
- addCheck(checks, '@geastack/core', packageExists(ctx.corePackageDir), ctx.corePackageDir, true)
382
- addCheck(checks, '@geastack/compiler', packageExists(ctx.compilerPackageDir), ctx.compilerPackageDir, true)
383
- addCheck(checks, 'project root', exists(ctx.projectRoot), ctx.projectRoot, true)
384
- addCheck(checks, 'web dev adapter', exists(ctx.scripts.webDev), ctx.scripts.webDev, false)
385
- addCheck(checks, 'web build adapter', exists(ctx.scripts.webBuild), ctx.scripts.webBuild, false)
386
- addCheck(checks, 'Android build adapter', exists(ctx.scripts.androidBuild), ctx.scripts.androidBuild, false)
387
- addCheck(checks, 'board script', exists(ctx.scripts.board), ctx.scripts.board, true)
388
- addCheck(checks, 'Node >= 20.19', nodeAtLeast(20, 19), process.version, true)
389
- const npmVersion = commandVersion('npm', ['--version'], io.env)
390
- const pythonCommand = commandVersion('python3', ['--version'], io.env) ? 'python3' : 'python'
391
- const pythonVersion = commandVersion(pythonCommand, ['--version'], io.env)
392
- const idfVersion = commandVersion('idf.py', ['--version'], io.env)
393
- const emccVersion = commandVersion('emcc', ['--version'], io.env)
394
- const xcodeVersion = commandVersion('xcodebuild', ['-version'], io.env)
395
- const adbVersion = commandVersion('adb', ['version'], io.env)
396
- const javacVersion = commandVersion('javac', ['--version'], io.env)
397
- const androidSdk = io.env.ANDROID_HOME || io.env.ANDROID_SDK_ROOT || path.join(process.env.HOME || '', 'Library', 'Android', 'sdk')
398
- addCheck(checks, 'npm', Boolean(npmVersion), npmVersion, false)
399
- addCheck(checks, 'Python', Boolean(pythonVersion), pythonVersion, false)
400
- addCheck(checks, 'ESP-IDF', Boolean(idfVersion) || Boolean(io.env.IDF_PATH), io.env.IDF_PATH || idfVersion, false)
401
- addCheck(checks, 'Emscripten', Boolean(emccVersion), emccVersion.split('\n')[0], false)
402
- addCheck(checks, 'Xcode', Boolean(xcodeVersion), xcodeVersion.split('\n')[0], false)
403
- addCheck(checks, 'Android SDK', exists(androidSdk), androidSdk, false)
404
- addCheck(checks, 'adb', Boolean(adbVersion), adbVersion.split('\n')[0], false)
405
- addCheck(checks, 'javac', Boolean(javacVersion), javacVersion.split('\n')[0], false)
406
-
407
- const boardFile = boardConfigPath(ctx)
408
- try {
409
- if (exists(boardFile)) readJson(boardFile)
410
- addCheck(checks, 'boards.json', exists(boardFile), exists(boardFile) ? boardFile : 'not configured', false)
411
- } catch (error) {
412
- addCheck(checks, 'boards.json', false, error.message, true)
413
- }
414
-
415
- const apps = discoverApps(ctx)
416
- addCheck(checks, 'app catalog', apps.length > 0, `${apps.length} app(s)`, true)
417
-
418
- const currentApp = (() => {
419
- try {
420
- return resolveRequestedApp(ctx, parsed, [])
421
- } catch {
422
- return null
423
- }
424
- })()
425
- if (currentApp) {
426
- const errors = validateApp(currentApp)
427
- addCheck(checks, `app manifest (${currentApp.id})`, errors.length === 0, errors.length === 0 ? currentApp.root : errors.join('; '), true)
428
- }
429
-
430
- const failedRequired = checks.filter((check) => check.required && !check.ok)
431
- const failedOptional = checks.filter((check) => !check.required && !check.ok)
432
-
433
- if (flag(parsed, 'json')) {
434
- io.stdout(JSON.stringify({ ok: failedRequired.length === 0, checks }, null, 2))
435
- } else {
436
- for (const check of checks) {
437
- const marker = check.ok ? '[ok]' : check.required ? '[fail]' : '[warn]'
438
- io.stdout(`${marker} ${check.name}: ${check.detail}`)
439
- }
440
- if (failedRequired.length > 0 || failedOptional.length > 0) {
441
- io.stdout('Setup guide: cli/docs/SETUP.md')
442
- }
443
- }
444
-
445
- if (failedRequired.length > 0) return ExitCode.missingDependency
446
- if (failedOptional.length > 0 && flag(parsed, 'strict')) return ExitCode.missingDependency
447
- return 0
448
- }
449
-
450
- function addCheck(checks, name, ok, detail, required) {
451
- checks.push({ name, ok: Boolean(ok), detail: detail || (ok ? 'ok' : 'missing'), required: Boolean(required) })
452
- }
453
-
454
- function packageExists(packageDir) {
455
- return Boolean(packageDir) && exists(path.join(packageDir, 'package.json'))
456
- }
457
-
458
- function requirePath(filePath, label) {
459
- if (!exists(filePath)) fail(`Missing ${label}: ${filePath}`, ExitCode.missingDependency)
460
- }
461
-
462
112
  function usage() {
463
- return `Usage:
464
- gea create <name> [--starter counter|empty|example] [--targets <list>]
465
- gea doctor [--strict] [--json]
466
- gea setup --board <alias>
467
- gea chips list [--json]
468
- gea chips info <chip> [--json]
469
- gea chips add <chip...> [--board <alias>] [--set chip.path=value]
470
- gea chips remove <chip...> [--board <alias>]
471
- gea dev [app] [--target web] [--port 5181]
472
- gea build [app] [--target web|macos|ios|android|<target>] [--board <alias>] [--output-tag <tag>]
473
- gea flash [app] --board <alias> [--monitor] [--port auto] [--manual-boot] [--no-reset] [--flash-baud <rate>]
474
- gea flash [app] --target android
475
- gea flash --bringup --board <alias> [--monitor] [--port auto]
476
- gea monitor --board <alias>
477
- gea monitor --target android
478
- gea screenshot [output.png] --board <alias> [--port auto] [--timeout <seconds>] [--legacy]
479
- gea ota [app] --board <alias> [--transport wifi|ble]
480
- gea list [apps|targets|boards] [--json]
481
- gea inspect [app] [--json]
482
-
483
- Global options:
484
- --boards-config <file>
485
- --dry-run
486
- `
113
+ return `gea ${version}
114
+
115
+ Usage:
116
+ gea create <name> scaffold a new Gea project
117
+ gea setup [--board <alias>] guided board setup, or configure a board's build
118
+ gea doctor [--json] [--strict] check packages and toolchains
119
+
120
+ Build and deploy (every device command takes --board <alias>, see gea boards list):
121
+ gea build --board <alias> [--app <id>] build firmware (ESP-IDF / Pico SDK / geaos)
122
+ gea flash --board <alias> [--app <id>] build + flash over USB [--monitor] [--manual-boot] [--no-reset] [--flash-baud N]
123
+ --slot ota_N [--image file] stage an app image into an OTA slot only
124
+ --slot-image ota_N=file ... provision several prebuilt images
125
+ --erase-slot ota_N | --restore-boot slot maintenance
126
+ gea run --board <alias> [--app <id>] build + flash + serial monitor
127
+ gea ota --board <alias> [--app <id>] build + WiFi OTA (transports.ota.host) [--monitor]
128
+ --transport ble [--device name] BLE OTA (macOS)
129
+ --slot ota_N [--boot] [--reboot] | --erase-slot ota_N
130
+ gea clean --board <alias> [--app <id>] remove build artifacts
131
+
132
+ Device access:
133
+ gea logs --board <alias> [--follow] log stream (WiFi when the board has an IP, else USB) [--transport auto|usb|wifi]
134
+ gea monitor --board <alias> raw USB serial monitor [--timestamps] [--log-file f]
135
+ gea screenshot [file.png] --board <alias> grab the screen [--transport auto|usb|wifi]
136
+ gea devctl <verb> ... --board <alias> GEADEV device control (gea devctl help)
137
+
138
+ Catalogs:
139
+ gea apps list|inspect|pack|index|launcher|icons|icon-sheet|apple-icons
140
+ gea boards list|show <alias>|add
141
+ gea targets list|show <id>
142
+ gea chips ... custom board composition from the chip catalog
143
+ gea heap-report [logs...] [--out file] [--map elf.map]
144
+
145
+ Global options: --project <dir> --boards-config <file> --dry-run --json`
487
146
  }
@@ -0,0 +1,119 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ import { resolveUsbSerialPort } from '../boards/usb.mjs'
6
+ import { CliError, ExitCode, fail } from '../errors.mjs'
7
+ import { formatCommand } from '../run.mjs'
8
+
9
+ // GeaOS phones and watches. The heavy lifting (docker cross-builds, boot
10
+ // image repacking, fastboot/BROM flashing) stays in the target project's
11
+ // scripts; the CLI resolves the board, the packages and the device, and
12
+ // hands them over as environment.
13
+
14
+ function geaosEnv(ctx, selection, env) {
15
+ const out = { ...env, GEA_BOARD_TARGET: selection.target, GEA_APPS_ROOT: ctx.projectRoot }
16
+ if (selection.telnetHost) {
17
+ out.GEA_BOARD_TELNET_HOST = selection.telnetHost
18
+ out.GEAOS_DEVICE_HOST = selection.telnetHost
19
+ }
20
+ if (selection.telnetPort) {
21
+ out.GEA_BOARD_TELNET_PORT = selection.telnetPort
22
+ out.GEAOS_DEVICE_PORT = selection.telnetPort
23
+ }
24
+ if (selection.fastbootSerial) {
25
+ out.GEA_BOARD_FASTBOOT_SERIAL = selection.fastbootSerial
26
+ out.GEAOS_FASTBOOT_SERIAL = selection.fastbootSerial
27
+ }
28
+ if (selection.usbSerial) out.GEA_BOARD_USB_SERIAL = selection.usbSerial
29
+ for (const [key, value] of Object.entries({
30
+ GEA_BOARD_MTK_WORKDIR: selection.mtkWorkdir,
31
+ GEA_BOARD_MTK_BOOT_SLOT: selection.mtkBootSlot,
32
+ GEA_BOARD_MTK_METHOD: selection.mtkMethod,
33
+ GEA_BOARD_MTK_MONITOR_GLOB: selection.mtkMonitorGlob
34
+ })) {
35
+ if (value) out[key] = value
36
+ }
37
+ // The device is addressed by its USB SERIAL, never by path order. A
38
+ // detached watch must still build, so an unresolved port stays empty and
39
+ // the flasher falls through to its own (BROM) path -- pinned to a glob only
40
+ // this serial can match, so another attached geaos device is never grabbed.
41
+ let port = selection.port
42
+ if (!port && selection.usbSerial) {
43
+ try {
44
+ port = resolveUsbSerialPort({ serial: selection.usbSerial })
45
+ } catch {
46
+ port = ''
47
+ }
48
+ }
49
+ if (port) {
50
+ out.GEAOS_SERIAL_PORT = port
51
+ out.GEAOS_MTK_GADGET_GLOB = port
52
+ out.GEA_BOARD_MTK_MONITOR_GLOB = port
53
+ } else if (selection.usbSerial) {
54
+ out.GEAOS_MTK_GADGET_GLOB = `/dev/cu.usbmodem*${selection.usbSerial}*`
55
+ out.GEA_BOARD_MTK_MONITOR_GLOB = `/dev/cu.usbmodem*${selection.usbSerial}*`
56
+ }
57
+ return out
58
+ }
59
+
60
+ function run(command, args, { cwd, env, dryRun, stdout, failureCode }) {
61
+ if (dryRun) {
62
+ stdout(formatCommand([command, ...args]))
63
+ return 0
64
+ }
65
+ if (!existsSync(command)) fail(`${command} was not found. Is the geaos target project available at ${cwd}?`, ExitCode.missingDependency)
66
+ const result = spawnSync(command, args, { cwd, env, stdio: 'inherit' })
67
+ if (result.error) throw result.error
68
+ if (result.status !== 0) throw new CliError(`ERROR: Command failed (${result.status ?? 1}): ${formatCommand([command, ...args])}`, failureCode)
69
+ return 0
70
+ }
71
+
72
+ export function runGeaos({ ctx, selection, action, app = null, positionals = [], env, dryRun = false, stdout }) {
73
+ const childEnv = geaosEnv(ctx, selection, env)
74
+ const targetDir = selection.targetDir
75
+ const appId = app?.id || positionals[0] || ''
76
+ const needApp = () => {
77
+ if (!appId) fail('A geaos app id is required (--app <id>).', ExitCode.usage)
78
+ return appId
79
+ }
80
+ if (selection.adapter === 'geaos-arm64') {
81
+ const script = path.join(targetDir, 'flash-geaos-arm64.sh')
82
+ switch (action) {
83
+ case 'build':
84
+ case 'flash':
85
+ case 'flash-monitor':
86
+ return run(script, [needApp()], { cwd: targetDir, env: { ...childEnv, GEAOS_ARM64_ACTION: action }, dryRun, stdout, failureCode: action === 'build' ? ExitCode.buildFailed : ExitCode.deployFailed })
87
+ case 'monitor':
88
+ return run(script, [appId || 'tic-tac-toe'], { cwd: targetDir, env: { ...childEnv, GEAOS_ARM64_ACTION: 'monitor' }, dryRun, stdout, failureCode: ExitCode.deployFailed })
89
+ default:
90
+ fail(`'${action}' is not supported by geaos-arm64 boards (build|flash|run|monitor).`, ExitCode.usage)
91
+ }
92
+ }
93
+ const deviceArgs = []
94
+ if (selection.telnetHost) deviceArgs.push('--host', selection.telnetHost)
95
+ if (selection.telnetPort) deviceArgs.push('--port', selection.telnetPort)
96
+ switch (action) {
97
+ case 'build':
98
+ return run(path.join(targetDir, 'build-geaos.sh'), [needApp()], { cwd: targetDir, env: childEnv, dryRun, stdout, failureCode: ExitCode.buildFailed })
99
+ case 'flash':
100
+ case 'flash-monitor':
101
+ return run(path.join(targetDir, 'flash-geaos.sh'), [needApp()], { cwd: targetDir, env: childEnv, dryRun, stdout, failureCode: ExitCode.deployFailed })
102
+ case 'flash-kernel':
103
+ return run(path.join(targetDir, 'flash-kernel.sh'), positionals, { cwd: targetDir, env: childEnv, dryRun, stdout, failureCode: ExitCode.deployFailed })
104
+ case 'bringup':
105
+ case 'probe':
106
+ case 'shell':
107
+ case 'push':
108
+ case 'screenshot':
109
+ case 'record':
110
+ case 'tap':
111
+ case 'drag':
112
+ case 'down':
113
+ case 'move':
114
+ case 'up':
115
+ return run(path.join(targetDir, 'geaos-device.py'), [...deviceArgs, action, ...positionals], { cwd: targetDir, env: childEnv, dryRun, stdout, failureCode: ExitCode.deployFailed })
116
+ default:
117
+ fail(`'${action}' is not supported by geaos-linux boards.`, ExitCode.usage)
118
+ }
119
+ }