@geastack/cli 0.1.52 → 0.1.54

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.
Files changed (44) hide show
  1. package/README.md +14 -1
  2. package/docs/ESP32-WAVESHARE-AMOLED-QUICKSTART.md +2 -2
  3. package/docs/NPX-COMMANDS.md +3 -1
  4. package/docs/SETUP.md +81 -15
  5. package/docs/SPEC.md +8 -5
  6. package/package.json +2 -3
  7. package/src/apps/app-index-writer.mjs +15 -0
  8. package/src/apps/apple-icons.mjs +147 -0
  9. package/src/apps/bundle-writer.mjs +156 -0
  10. package/src/apps/launcher-catalog.mjs +114 -0
  11. package/src/apps/openai-icons.mjs +356 -0
  12. package/src/apps/zip-writer.mjs +104 -0
  13. package/src/ble/ble-ota.swift +291 -0
  14. package/src/boards/config.mjs +114 -0
  15. package/src/boards/custom-target.mjs +309 -0
  16. package/src/boards/resolve.mjs +145 -0
  17. package/src/boards/targets.mjs +45 -0
  18. package/src/boards/usb.mjs +252 -0
  19. package/src/chips.mjs +1 -1
  20. package/src/commands/apps.mjs +204 -0
  21. package/src/commands/board.mjs +402 -0
  22. package/src/commands/boards.mjs +334 -0
  23. package/src/commands/doctor.mjs +91 -0
  24. package/src/context.mjs +50 -54
  25. package/src/device/device.mjs +96 -0
  26. package/src/device/image.mjs +115 -0
  27. package/src/device/serial.mjs +430 -0
  28. package/src/device/wifi.mjs +190 -0
  29. package/src/esp32/build.mjs +321 -0
  30. package/src/esp32/capabilities.mjs +54 -0
  31. package/src/esp32/flash.mjs +181 -0
  32. package/src/esp32/idf-env.mjs +171 -0
  33. package/src/esp32/ota.mjs +80 -0
  34. package/src/esp32/partitions.mjs +59 -0
  35. package/src/esp32/sdkconfig.mjs +103 -0
  36. package/src/esp32/wifi-config.mjs +72 -0
  37. package/src/gea.mjs +89 -427
  38. package/src/geaos/adapter.mjs +119 -0
  39. package/src/heap-report.mjs +1276 -0
  40. package/src/manifest.mjs +135 -36
  41. package/src/rp2350/adapter.mjs +155 -0
  42. package/src/serial-devices.mjs +32 -20
  43. package/src/setup-wizard.mjs +11 -12
  44. package/src/taurus/adapter.mjs +52 -0
package/src/gea.mjs CHANGED
@@ -1,28 +1,21 @@
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, heapReportCommand, targetsCommand } from './commands/apps.mjs'
6
+ import { boardsCommand } from './commands/boards.mjs'
7
+ import { buildCommand, cleanCommand, devctlCommand, flashCommand, geaosDeviceCommand, logsCommand, monitorCommand, otaCommand, screenshotCommand } from './commands/board.mjs'
8
+ import { doctorCommand } from './commands/doctor.mjs'
9
+ import { createContext } from './context.mjs'
10
+ import { runCreateGeastack } from './create-geastack.mjs'
8
11
  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'
12
+ import { knownPlatforms } from './manifest.mjs'
21
13
  import { runSetupWizard } from './setup-wizard.mjs'
22
- import { commandVersion, nodeAtLeast } from './toolchain.mjs'
23
14
 
24
15
  const version = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
25
16
 
17
+ const geaosDeviceActions = new Set(['bringup', 'probe', 'shell', 'push', 'record', 'tap', 'drag', 'down', 'move', 'up', 'flash-kernel'])
18
+
26
19
  export async function runGea(argv, io = {}) {
27
20
  const parsed = parseArgs(argv)
28
21
  const stdout = io.stdout || console.log
@@ -50,438 +43,107 @@ export async function runGea(argv, io = {}) {
50
43
 
51
44
  const ctx = createContext(parsed, env, cwd)
52
45
  const rest = parsed.positionals.slice(1)
46
+ // probeSerialDevice lets tests answer `gea boards discover` without a port.
47
+ const options = { stdout, stderr, env, stdin, output, prompt, probeSerialDevice: io.probeSerialDevice }
48
+
49
+ // A platform name in --target (web, macos, ...) is not a board.
50
+ const target = option(parsed, 'target', '')
51
+ if (target && knownPlatforms.includes(target) && ['build', 'flash', 'run', 'monitor', 'ota'].includes(command)) {
52
+ if (target === 'esp32' || target === 'rp2350' || target === 'geaos') {
53
+ fail(`--target ${target} names a platform; pass --board <alias> (gea boards list) or --target <target id> (gea targets list).`, ExitCode.usage)
54
+ }
55
+ fail(`The ${target} build is not driven by gea; run its build script in the ${target} target project.`, ExitCode.usage)
56
+ }
53
57
 
54
58
  switch (command) {
55
59
  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 })
60
+ return doctorCommand(ctx, parsed, rest, options)
61
61
  case 'setup':
62
- return setup(ctx, parsed, { stdout, env, stdin, output, prompt })
62
+ return option(parsed, 'board') || option(parsed, 'target')
63
+ ? buildCommand(ctx, { ...parsed, options: { ...parsed.options, 'configure-only': true } }, rest, options)
64
+ : runSetupWizard(ctx, parsed, options)
63
65
  case 'chips':
64
- return runChips(ctx, parsed, rest, { stdout, env, stdin, output, prompt })
66
+ return runChips(ctx, parsed, rest, options)
67
+ case 'build':
68
+ return buildCommand(ctx, parsed, rest, options)
69
+ case 'clean':
70
+ return cleanCommand(ctx, parsed, rest, options)
65
71
  case 'flash':
66
- return flash(ctx, parsed, rest, { stdout, env })
72
+ return flashCommand(ctx, parsed, rest, options, { monitor: flag(parsed, 'monitor') })
73
+ case 'run':
74
+ return flashCommand(ctx, parsed, rest, options, { monitor: true })
67
75
  case 'monitor':
68
- return monitor(ctx, parsed, rest, { stdout, env })
76
+ return monitorCommand(ctx, parsed, rest, options)
77
+ case 'logs':
78
+ case 'tail':
79
+ return logsCommand(ctx, parsed, rest, options)
69
80
  case 'screenshot':
70
- return screenshot(ctx, parsed, rest, { stdout, env })
81
+ return screenshotCommand(ctx, parsed, rest, options)
71
82
  case 'ota':
72
- return ota(ctx, parsed, rest, { stdout, env })
83
+ return otaCommand(ctx, parsed, rest, options)
84
+ case 'devctl':
85
+ return devctlCommand(ctx, parsed, rest, options)
86
+ case 'apps':
87
+ return appsCommand(ctx, parsed, rest, options)
88
+ case 'boards':
89
+ return boardsCommand(ctx, parsed, rest, options)
90
+ case 'targets':
91
+ return targetsCommand(ctx, parsed, rest, options)
92
+ case 'heap-report':
93
+ return heapReportCommand(ctx, parsed, rest, options)
73
94
  case 'list':
74
- return list(ctx, parsed, rest, { stdout })
95
+ return legacyList(ctx, parsed, rest, options)
75
96
  case 'inspect':
76
- return inspect(ctx, parsed, rest, { stdout })
97
+ return appsCommand(ctx, parsed, ['inspect', ...rest], options)
77
98
  default:
99
+ if (geaosDeviceActions.has(command)) return geaosDeviceCommand(ctx, parsed, [command, ...rest], options)
78
100
  stderr(`Unknown command: ${command}`)
79
101
  stdout(usage())
80
102
  return 1
81
103
  }
82
104
  }
83
105
 
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) {
106
+ function legacyList(ctx, parsed, rest, options) {
335
107
  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
- }
108
+ if (subject === 'apps') return appsCommand(ctx, parsed, ['list'], options)
109
+ if (subject === 'targets') return targetsCommand(ctx, parsed, ['list'], options)
110
+ if (subject === 'boards') return boardsCommand(ctx, parsed, ['list'], options)
357
111
  fail(`Unknown list subject '${subject}'. Expected apps, targets, or boards.`, ExitCode.usage)
358
112
  }
359
113
 
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
114
  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
- `
115
+ return `gea ${version}
116
+
117
+ Usage:
118
+ gea create <name> scaffold a new Gea project
119
+ gea setup [--board <alias>] guided board setup, or configure a board's build
120
+ gea doctor [--json] [--strict] check packages and toolchains
121
+
122
+ Build and deploy (every device command takes --board <alias>, see gea boards list):
123
+ gea build --board <alias> [--app <id>] build firmware (ESP-IDF / Pico SDK / geaos)
124
+ gea flash --board <alias> [--app <id>] build + flash over USB [--monitor] [--manual-boot] [--no-reset] [--flash-baud N]
125
+ --slot ota_N [--image file] stage an app image into an OTA slot only
126
+ --slot-image ota_N=file ... provision several prebuilt images
127
+ --erase-slot ota_N | --restore-boot slot maintenance
128
+ gea run --board <alias> [--app <id>] build + flash + serial monitor
129
+ gea ota --board <alias> [--app <id>] build + WiFi OTA (transports.ota.host) [--monitor]
130
+ --transport ble [--device name] BLE OTA (macOS)
131
+ --slot ota_N [--boot] [--reboot] | --erase-slot ota_N
132
+ gea clean --board <alias> [--app <id>] remove build artifacts
133
+
134
+ Device access:
135
+ gea logs --board <alias> [--follow] log stream (WiFi when the board has an IP, else USB) [--transport auto|usb|wifi]
136
+ gea monitor --board <alias> raw USB serial monitor [--timestamps] [--log-file f]
137
+ gea screenshot [file.png] --board <alias> grab the screen [--transport auto|usb|wifi]
138
+ gea devctl <verb> ... --board <alias> GEADEV device control (gea devctl help)
139
+
140
+ Catalogs:
141
+ gea apps list|inspect|pack|index|launcher|icons|icon-sheet|apple-icons
142
+ gea boards list|show|add|set|remove|rename|discover (gea boards help)
143
+ gea targets list|show <id>
144
+ gea chips ... custom board composition from the chip catalog
145
+ gea heap-report [logs...] [--out file] [--map elf.map]
146
+
147
+ Global options: --project <dir> --boards-config <file> --global|--local (boards writes) --dry-run --json
148
+ Board aliases: ~/.geastack/boards.json (this machine) + <project>/.gea/boards.json (overrides)`
487
149
  }
@@ -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
+ }