@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
@@ -0,0 +1,309 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ // A composed board: a JSON definition (chips from @geastack/chips plus pins
5
+ // and buses) that extends a built-in base target. The CLI turns it into a
6
+ // generated board.h + target.cmake inside the app's build directory before
7
+ // IDF configures, so the target project only ever includes generated files.
8
+
9
+ const supportedBase = 'esp32-s3'
10
+
11
+ function object(value, label) {
12
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`)
13
+ return value
14
+ }
15
+
16
+ function text(value, label) {
17
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${label} must be a non-empty string.`)
18
+ return value.trim()
19
+ }
20
+
21
+ function integer(value, label, { min = 0, max = 48 } = {}) {
22
+ if (!Number.isInteger(value) || value < min || value > max) {
23
+ throw new Error(`${label} must be an integer from ${min} through ${max}.`)
24
+ }
25
+ return value
26
+ }
27
+
28
+ function pin(value, label, { optional = false } = {}) {
29
+ if (optional && (value === null || value === undefined || value === 'none')) return -1
30
+ return integer(value, label)
31
+ }
32
+
33
+ function exact(value, expected, label) {
34
+ const actual = text(value, label).toLowerCase()
35
+ if (actual !== expected) {
36
+ throw new Error(`${label} '${actual}' is not supported by the ${supportedBase} base. Supported value: ${expected}.`)
37
+ }
38
+ return actual
39
+ }
40
+
41
+ export function loadChipCatalogFromDir(chipsDir) {
42
+ const packageDir = text(chipsDir, 'chips package directory')
43
+ const catalogPath = path.join(packageDir, 'catalog.json')
44
+ if (!existsSync(catalogPath)) throw new Error(`Chip catalog not found: ${catalogPath}`)
45
+ const catalog = object(JSON.parse(readFileSync(catalogPath, 'utf8')), 'Chip catalog')
46
+ if (catalog.schemaVersion !== 1) throw new Error(`Unsupported chip catalog schema: ${catalog.schemaVersion}`)
47
+ return object(catalog.chips, 'Chip catalog entries')
48
+ }
49
+
50
+ function selectedChip(chips, role, category, adapter, mcu, catalog) {
51
+ const selection = object(chips[role], `chips.${role}`)
52
+ const driver = text(selection.driver || selection.controller, `chips.${role}.driver`).toLowerCase()
53
+ const descriptor = object(catalog[driver], `Catalog entry for '${driver}'`)
54
+ if (descriptor.category !== category) {
55
+ throw new Error(`Chip '${driver}' is a ${descriptor.category}, so it cannot fill the ${role} role.`)
56
+ }
57
+ const interfaceName = text(selection.interface, `chips.${role}.interface`).toLowerCase()
58
+ if (!Array.isArray(descriptor.interfaces) || !descriptor.interfaces.includes(interfaceName)) {
59
+ throw new Error(`Chip '${driver}' does not support the '${interfaceName}' interface.`)
60
+ }
61
+ const adapterInfo = descriptor.adapters?.[adapter]
62
+ if (!adapterInfo) throw new Error(`Chip '${driver}' has no ${adapter} binding in the installed catalog.`)
63
+ if (Array.isArray(adapterInfo.mcus) && !adapterInfo.mcus.includes(mcu)) {
64
+ throw new Error(`Chip '${driver}' does not support MCU '${mcu}' through ${adapter}.`)
65
+ }
66
+ return {
67
+ ...selection,
68
+ driver,
69
+ interface: interfaceName,
70
+ nativeSources: Array.isArray(descriptor.sources) ? descriptor.sources : [],
71
+ bindingSources: Array.isArray(adapterInfo.bindingSources) ? adapterInfo.bindingSources : []
72
+ }
73
+ }
74
+
75
+ export function normalizeCustomTarget(raw, catalog) {
76
+ const definition = object(raw, 'Target definition')
77
+ const id = text(definition.id, 'id')
78
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) throw new Error('id may contain lowercase letters, digits, dots, underscores, and hyphens.')
79
+ const base = exact(definition.extends, supportedBase, 'extends')
80
+ const mcu = exact(definition.mcu, 'esp32s3', 'mcu')
81
+ const adapter = definition.adapter ? exact(definition.adapter, 'esp32-idf', 'adapter') : 'esp32-idf'
82
+ const chips = object(definition.chips, 'chips')
83
+ const display = object(selectedChip(chips, 'display', 'display', adapter, mcu, catalog), 'chips.display')
84
+ const touch = object(selectedChip(chips, 'touch', 'touch', adapter, mcu, catalog), 'chips.touch')
85
+ const power = object(selectedChip(chips, 'power', 'power', adapter, mcu, catalog), 'chips.power')
86
+ const imu = object(selectedChip(chips, 'imu', 'imu', adapter, mcu, catalog), 'chips.imu')
87
+ const audio = object(selectedChip(chips, 'audio', 'audio', adapter, mcu, catalog), 'chips.audio')
88
+ const buses = object(definition.buses, 'buses')
89
+ const i2c = object(buses.i2c, 'buses.i2c')
90
+ const storage = object(definition.storage, 'storage')
91
+ const microSD = object(storage.microSD, 'storage.microSD')
92
+ const controls = object(definition.controls, 'controls')
93
+ const launcherButton = object(controls.launcherButton, 'controls.launcherButton')
94
+
95
+ const displayPins = object(display.pins, 'chips.display.pins')
96
+ const touchPins = object(touch.pins, 'chips.touch.pins')
97
+ const audioPins = object(audio.pins, 'chips.audio.pins')
98
+ const storagePins = object(microSD.pins, 'storage.microSD.pins')
99
+
100
+ const spiHost = String(display.spiHost).toLowerCase()
101
+ if (!['spi2', 'spi3'].includes(spiHost)) throw new Error("chips.display.spiHost must be 'spi2' or 'spi3'.")
102
+
103
+ const target = {
104
+ id,
105
+ extends: base,
106
+ adapter,
107
+ mcu,
108
+ buses: {
109
+ i2c: {
110
+ sda: pin(i2c.sda, 'buses.i2c.sda'),
111
+ scl: pin(i2c.scl, 'buses.i2c.scl')
112
+ }
113
+ },
114
+ chips: {
115
+ display: {
116
+ driver: display.driver,
117
+ interface: exact(display.interface, 'qspi', 'chips.display.interface'),
118
+ width: integer(display.width, 'chips.display.width', { min: 1, max: 4096 }),
119
+ height: integer(display.height, 'chips.display.height', { min: 1, max: 4096 }),
120
+ spiHost,
121
+ nativeSources: display.nativeSources,
122
+ bindingSources: display.bindingSources,
123
+ pins: {
124
+ cs: pin(displayPins.cs, 'chips.display.pins.cs'),
125
+ pclk: pin(displayPins.pclk, 'chips.display.pins.pclk'),
126
+ data0: pin(displayPins.data0, 'chips.display.pins.data0'),
127
+ data1: pin(displayPins.data1, 'chips.display.pins.data1'),
128
+ data2: pin(displayPins.data2, 'chips.display.pins.data2'),
129
+ data3: pin(displayPins.data3, 'chips.display.pins.data3'),
130
+ reset: pin(displayPins.reset, 'chips.display.pins.reset'),
131
+ te: pin(displayPins.te, 'chips.display.pins.te', { optional: true })
132
+ }
133
+ },
134
+ touch: {
135
+ driver: touch.driver,
136
+ interface: exact(touch.interface, 'i2c', 'chips.touch.interface'),
137
+ nativeSources: touch.nativeSources,
138
+ bindingSources: touch.bindingSources,
139
+ pins: {
140
+ reset: pin(touchPins.reset, 'chips.touch.pins.reset'),
141
+ interrupt: pin(touchPins.interrupt, 'chips.touch.pins.interrupt')
142
+ }
143
+ },
144
+ power: {
145
+ driver: power.driver,
146
+ interface: exact(power.interface, 'i2c', 'chips.power.interface'),
147
+ nativeSources: power.nativeSources,
148
+ bindingSources: power.bindingSources
149
+ },
150
+ imu: {
151
+ driver: imu.driver,
152
+ interface: exact(imu.interface, 'i2c', 'chips.imu.interface'),
153
+ nativeSources: imu.nativeSources,
154
+ bindingSources: imu.bindingSources
155
+ },
156
+ audio: {
157
+ driver: audio.driver,
158
+ interface: exact(audio.interface, 'i2s', 'chips.audio.interface'),
159
+ nativeSources: audio.nativeSources,
160
+ bindingSources: audio.bindingSources,
161
+ pins: {
162
+ mclk: pin(audioPins.mclk, 'chips.audio.pins.mclk'),
163
+ bclk: pin(audioPins.bclk, 'chips.audio.pins.bclk'),
164
+ ws: pin(audioPins.ws, 'chips.audio.pins.ws'),
165
+ dout: pin(audioPins.dout, 'chips.audio.pins.dout'),
166
+ din: pin(audioPins.din, 'chips.audio.pins.din'),
167
+ powerAmplifier: pin(audioPins.powerAmplifier, 'chips.audio.pins.powerAmplifier')
168
+ }
169
+ }
170
+ },
171
+ storage: {
172
+ microSD: {
173
+ interface: exact(microSD.interface, 'sdmmc-1bit', 'storage.microSD.interface'),
174
+ pins: {
175
+ clk: pin(storagePins.clk, 'storage.microSD.pins.clk', { optional: true }),
176
+ cmd: pin(storagePins.cmd, 'storage.microSD.pins.cmd', { optional: true }),
177
+ data0: pin(storagePins.data0, 'storage.microSD.pins.data0', { optional: true })
178
+ }
179
+ }
180
+ },
181
+ controls: {
182
+ launcherButton: {
183
+ pin: pin(launcherButton.pin, 'controls.launcherButton.pin', { optional: true }),
184
+ activeLevel: integer(launcherButton.activeLevel, 'controls.launcherButton.activeLevel', { min: 0, max: 1 })
185
+ }
186
+ }
187
+ }
188
+ validatePinAssignments(target)
189
+ return target
190
+ }
191
+
192
+ function validatePinAssignments(target) {
193
+ const pins = [
194
+ ['I2C SDA', target.buses.i2c.sda],
195
+ ['I2C SCL', target.buses.i2c.scl],
196
+ ['display CS', target.chips.display.pins.cs],
197
+ ['display PCLK', target.chips.display.pins.pclk],
198
+ ['display DATA0', target.chips.display.pins.data0],
199
+ ['display DATA1', target.chips.display.pins.data1],
200
+ ['display DATA2', target.chips.display.pins.data2],
201
+ ['display DATA3', target.chips.display.pins.data3],
202
+ ['display reset', target.chips.display.pins.reset],
203
+ ['display TE', target.chips.display.pins.te],
204
+ ['touch reset', target.chips.touch.pins.reset],
205
+ ['touch interrupt', target.chips.touch.pins.interrupt],
206
+ ['audio MCLK', target.chips.audio.pins.mclk],
207
+ ['audio BCLK', target.chips.audio.pins.bclk],
208
+ ['audio WS', target.chips.audio.pins.ws],
209
+ ['audio DOUT', target.chips.audio.pins.dout],
210
+ ['audio DIN', target.chips.audio.pins.din],
211
+ ['audio amplifier', target.chips.audio.pins.powerAmplifier],
212
+ ['microSD CLK', target.storage.microSD.pins.clk],
213
+ ['microSD CMD', target.storage.microSD.pins.cmd],
214
+ ['microSD DATA0', target.storage.microSD.pins.data0],
215
+ ['launcher button', target.controls.launcherButton.pin]
216
+ ]
217
+ const used = new Map()
218
+ for (const [label, value] of pins) {
219
+ if (value < 0) continue
220
+ const previous = used.get(value)
221
+ if (previous) throw new Error(`GPIO ${value} is assigned to both ${previous} and ${label}.`)
222
+ used.set(value, label)
223
+ }
224
+ }
225
+
226
+ function gpio(value) {
227
+ return value < 0 ? 'GPIO_NUM_NC' : `GPIO_NUM_${value}`
228
+ }
229
+
230
+ export function renderBoardHeader(target) {
231
+ const { i2c } = target.buses
232
+ const { display, touch, audio } = target.chips
233
+ const sd = target.storage.microSD
234
+ const launcher = target.controls.launcherButton
235
+ return `#pragma once
236
+
237
+ #include "driver/gpio.h"
238
+ #include "driver/i2s_types.h"
239
+ #include "driver/spi_master.h"
240
+
241
+ namespace gea::platform::board {
242
+
243
+ struct I2cBusConfig { gpio_num_t sda; gpio_num_t scl; };
244
+ struct Co5300DisplayConfig { spi_host_device_t spiHost; gpio_num_t cs; gpio_num_t pclk; gpio_num_t data0; gpio_num_t data1; gpio_num_t data2; gpio_num_t data3; gpio_num_t reset; gpio_num_t te; };
245
+ struct Ft3168TouchConfig { gpio_num_t reset; gpio_num_t interrupt; };
246
+ struct Es8311AudioConfig { int i2sPort; gpio_num_t mclk; gpio_num_t bclk; gpio_num_t ws; gpio_num_t dout; gpio_num_t din; gpio_num_t powerAmplifier; };
247
+ struct SdMmcConfig { gpio_num_t clk; gpio_num_t cmd; gpio_num_t data0; };
248
+ struct LauncherButtonConfig { gpio_num_t pin; int activeLevel; };
249
+
250
+ inline constexpr I2cBusConfig i2c{ .sda = ${gpio(i2c.sda)}, .scl = ${gpio(i2c.scl)} };
251
+ inline constexpr Co5300DisplayConfig display{
252
+ .spiHost = ${display.spiHost === 'spi3' ? 'SPI3_HOST' : 'SPI2_HOST'}, .cs = ${gpio(display.pins.cs)}, .pclk = ${gpio(display.pins.pclk)},
253
+ .data0 = ${gpio(display.pins.data0)}, .data1 = ${gpio(display.pins.data1)},
254
+ .data2 = ${gpio(display.pins.data2)}, .data3 = ${gpio(display.pins.data3)},
255
+ .reset = ${gpio(display.pins.reset)}, .te = ${gpio(display.pins.te)}
256
+ };
257
+ inline constexpr Ft3168TouchConfig touch{ .reset = ${gpio(touch.pins.reset)}, .interrupt = ${gpio(touch.pins.interrupt)} };
258
+ inline constexpr Es8311AudioConfig audio{
259
+ .i2sPort = I2S_NUM_AUTO, .mclk = ${gpio(audio.pins.mclk)}, .bclk = ${gpio(audio.pins.bclk)},
260
+ .ws = ${gpio(audio.pins.ws)}, .dout = ${gpio(audio.pins.dout)}, .din = ${gpio(audio.pins.din)},
261
+ .powerAmplifier = ${gpio(audio.pins.powerAmplifier)}
262
+ };
263
+ inline constexpr SdMmcConfig storage{ .clk = ${gpio(sd.pins.clk)}, .cmd = ${gpio(sd.pins.cmd)}, .data0 = ${gpio(sd.pins.data0)} };
264
+ inline constexpr LauncherButtonConfig launcherButton{ .pin = ${gpio(launcher.pin)}, .activeLevel = ${launcher.activeLevel} };
265
+
266
+ } // namespace gea::platform::board
267
+ `
268
+ }
269
+
270
+ function cmakeQuote(value) {
271
+ return `"${String(value).replace(/\\/g, '/').replace(/"/g, '\\"')}"`
272
+ }
273
+
274
+ export function renderTargetCmake(target, includeDir) {
275
+ const { display, touch, power, imu, audio } = target.chips
276
+ const chipSource = (source) => ` "\${GEA_CHIPS}/${source}"`
277
+ const bindingSource = (source) => ` "\${GEA_EMBEDDED_ROOT}/targets/esp32/${source}"`
278
+ const displaySources = [...display.nativeSources.map(chipSource), ...display.bindingSources.map(bindingSource)]
279
+ const peripheralSources = [power, imu, touch, audio]
280
+ .flatMap((chip) => [...chip.nativeSources.map(chipSource), ...chip.bindingSources.map(bindingSource)])
281
+ return `set(GEA_CUSTOM_TARGET_ACTIVE 1)
282
+ set(GEA_CUSTOM_TARGET_INCLUDE_DIR ${cmakeQuote(includeDir)})
283
+ set(GEA_CUSTOM_TARGET_DISPLAY_SOURCES
284
+ ${displaySources.join('\n')}
285
+ )
286
+ set(GEA_CUSTOM_TARGET_PERIPHERAL_SOURCES
287
+ ${peripheralSources.join('\n')}
288
+ )
289
+ set(GEA_CUSTOM_TARGET_COMPILE_DEFINITIONS
290
+ GEA_EMBEDDED_DISPLAY_WIDTH=${display.width}
291
+ GEA_EMBEDDED_DISPLAY_HEIGHT=${display.height}
292
+ )
293
+ `
294
+ }
295
+
296
+ export function writeCustomTarget({ definitionPath, outDir, catalog }) {
297
+ const target = normalizeCustomTarget(JSON.parse(readFileSync(definitionPath, 'utf8')), catalog)
298
+ mkdirSync(outDir, { recursive: true })
299
+ const headerPath = path.join(outDir, 'board.h')
300
+ const cmakePath = path.join(outDir, 'target.cmake')
301
+ writeIfChanged(headerPath, renderBoardHeader(target))
302
+ writeIfChanged(cmakePath, renderTargetCmake(target, outDir))
303
+ return { target, headerPath, cmakePath }
304
+ }
305
+
306
+ function writeIfChanged(file, contents) {
307
+ if (existsSync(file) && readFileSync(file, 'utf8') === contents) return
308
+ writeFileSync(file, contents)
309
+ }
@@ -0,0 +1,145 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ import { boardConfigOrigins, boardConfigPath, loadBoardConfig, normalizeBoardConfig } from './config.mjs'
5
+ import { loadTargets } from './targets.mjs'
6
+ import { resolveUsbSerialPort } from './usb.mjs'
7
+
8
+ export const usbSerialAdapters = new Set(['esp32-idf', 'rp2350-pico', 'taurus-s3'])
9
+ export const geaosAdapters = new Set(['geaos-linux', 'geaos-arm64'])
10
+
11
+ function boardTransport(board, name) {
12
+ return board?.transports?.[name] || {}
13
+ }
14
+
15
+ function isAuto(value) {
16
+ return !value || /^auto$/i.test(value) || value === '<auto>'
17
+ }
18
+
19
+ // Turns a board alias (or a bare target id) into everything a command needs:
20
+ // the target project directory, its adapter, chip/flash metadata and the
21
+ // transports the command asked for. `needs.usbPort` resolves the board's USB
22
+ // serial to today's /dev port; `needs.otaHost` resolves transports.ota.host.
23
+ // Commands state what they need instead of the resolver guessing from names.
24
+ export function resolveBoardSelection({
25
+ ctx = null,
26
+ boardName = '',
27
+ targetName = '',
28
+ requestedPort = '',
29
+ requestedHost = '',
30
+ needs = {},
31
+ targets = ctx ? loadTargets(ctx) : {},
32
+ config = ctx ? loadBoardConfig(ctx) : {},
33
+ // A targetDefinition is relative to the file its alias came from, which
34
+ // differs between the home and project tiers.
35
+ configDir = ctx ? path.dirname((boardName && boardConfigOrigins(ctx).get(boardName)) || boardConfigPath(ctx)) : process.cwd(),
36
+ usbSerialResolver = resolveUsbSerialPort,
37
+ deferUsbPort = false
38
+ } = {}) {
39
+ const boards = normalizeBoardConfig(config)
40
+ const board = boardName ? boards[boardName] : null
41
+ if (boardName && !board) {
42
+ throw new Error(`Unknown board '${boardName}'. Run gea boards list, or gea boards add to register it.`)
43
+ }
44
+
45
+ let target = board?.target || targetName || ''
46
+ if (!target) throw new Error('No board selected. Pass --board <alias> (gea boards list) or --target <id>.')
47
+ let targetBase = target
48
+ let targetDefinition = ''
49
+ let definition = null
50
+ if (board?.targetDefinition) {
51
+ targetDefinition = path.resolve(configDir, board.targetDefinition)
52
+ if (!existsSync(targetDefinition)) {
53
+ throw new Error(`Target definition for board '${boardName}' was not found: ${targetDefinition}`)
54
+ }
55
+ definition = JSON.parse(readFileSync(targetDefinition, 'utf8'))
56
+ if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {
57
+ throw new Error(`Target definition must be a JSON object: ${targetDefinition}`)
58
+ }
59
+ if (!definition.id) throw new Error(`Target definition is missing id: ${targetDefinition}`)
60
+ if (!definition.extends) throw new Error(`Target definition is missing extends: ${targetDefinition}`)
61
+ if (board.target && board.target !== definition.id) {
62
+ throw new Error(`Board '${boardName}' selects target '${board.target}', but ${targetDefinition} defines '${definition.id}'.`)
63
+ }
64
+ target = definition.id
65
+ targetBase = definition.extends
66
+ }
67
+ const targetInfo = targets[targetBase] || {}
68
+ const adapter = board?.adapter || definition?.adapter || targetInfo.adapter || ''
69
+ if (!adapter) throw new Error(`Unknown target '${target}'. It is not in targets.json and the board declares no adapter.`)
70
+
71
+ const usb = boardTransport(board, 'usbSerial')
72
+ if (usb.path) {
73
+ throw new Error(`Board '${boardName}' uses transports.usbSerial.path, which is no longer supported; set transports.usbSerial.serial instead.`)
74
+ }
75
+ let port = ''
76
+ let host = ''
77
+ const usbSerial = usb.serial || ''
78
+
79
+ if (needs.usbPort && usbSerialAdapters.has(adapter)) {
80
+ if (!isAuto(requestedPort)) {
81
+ port = requestedPort
82
+ } else if (usbSerial) {
83
+ if (!deferUsbPort) port = usbSerialResolver({ serial: usbSerial })
84
+ } else if (boardName) {
85
+ // A WiFi-only board hits this on monitor/screenshot. Point at the
86
+ // cable-free command instead of dead-ending on USB.
87
+ const wireless = boardTransport(board, 'ota').host
88
+ ? ` This board has transports.ota.host, so 'gea logs --board ${boardName}' and 'gea screenshot --board ${boardName}' work with no cable.`
89
+ : ''
90
+ throw new Error(`Board '${boardName}' does not define transports.usbSerial.serial, and no USB port was passed.${wireless}`)
91
+ }
92
+ }
93
+
94
+ // geaos devices are identified by USB serial too, but tolerantly: a build
95
+ // with the watch detached must still work, so an unresolved port stays
96
+ // empty and the adapter errors at the point it needs the device.
97
+ if (needs.usbPort && geaosAdapters.has(adapter)) {
98
+ if (!isAuto(requestedPort)) {
99
+ port = requestedPort
100
+ } else if (usbSerial && !deferUsbPort) {
101
+ try {
102
+ port = usbSerialResolver({ serial: usbSerial })
103
+ } catch {
104
+ port = ''
105
+ }
106
+ }
107
+ }
108
+
109
+ if (needs.otaHost) {
110
+ if (!isAuto(requestedHost)) {
111
+ host = requestedHost
112
+ } else {
113
+ host = boardTransport(board, 'ota').host || ''
114
+ if (!host) throw new Error(`Board '${boardName || target}' does not define transports.ota.host, and no --host was passed.`)
115
+ }
116
+ }
117
+
118
+ const selection = {
119
+ boardName,
120
+ target,
121
+ adapter,
122
+ targetDir: board?.targetDir || targetInfo.targetDir || '',
123
+ flashSize: board?.flashSize || targetInfo.flashSize || '',
124
+ appPlatform: board?.appPlatform || targetInfo.appPlatform || '',
125
+ compatibleAppPlatforms: Array.isArray(targetInfo.compatibleAppPlatforms) ? targetInfo.compatibleAppPlatforms : [],
126
+ idfTarget: board?.idfTarget || targetInfo.idfTarget || '',
127
+ esptoolChip: board?.esptoolChip || targetInfo.esptoolChip || '',
128
+ mainTaskStackSize: board?.mainTaskStackSize || targetInfo.mainTaskStackSize || '',
129
+ ipcTaskStackSize: board?.ipcTaskStackSize || targetInfo.ipcTaskStackSize || '',
130
+ port,
131
+ host,
132
+ usbSerial,
133
+ usbRestartAfterFlash: usb.restartAfterFlash || '',
134
+ otaHost: boardTransport(board, 'ota').host || '',
135
+ telnetHost: boardTransport(board, 'telnet').host || board?.telnetHost || targetInfo.telnetHost || '',
136
+ telnetPort: String(boardTransport(board, 'telnet').port || board?.telnetPort || targetInfo.telnetPort || ''),
137
+ fastbootSerial: boardTransport(board, 'fastboot').serial || board?.fastbootSerial || '',
138
+ mtkWorkdir: boardTransport(board, 'mtk').workdir || board?.mtkWorkdir || '',
139
+ mtkBootSlot: boardTransport(board, 'mtk').bootSlot || board?.mtkBootSlot || '',
140
+ mtkMethod: boardTransport(board, 'mtk').method || board?.mtkMethod || '',
141
+ mtkMonitorGlob: boardTransport(board, 'mtk').monitorGlob || board?.mtkMonitorGlob || '',
142
+ targetDefinition
143
+ }
144
+ return selection
145
+ }
@@ -0,0 +1,45 @@
1
+ import path from 'node:path'
2
+
3
+ import { ExitCode, fail } from '../errors.mjs'
4
+ import { exists, readJson } from '../fs-utils.mjs'
5
+
6
+ // Built-in targets are DATA shipped by @geastack/targets (targets.json at the
7
+ // package root). Each entry names an adapter and where its project lives; the
8
+ // project may sit in another package (geaos boards live in @geastack/geaos).
9
+ export function loadTargets(ctx) {
10
+ if (!ctx.targetsRoot) return {}
11
+ const file = path.join(ctx.targetsRoot, 'targets.json')
12
+ if (!exists(file)) return {}
13
+ const raw = readJson(file)
14
+ const out = {}
15
+ for (const [id, entry] of Object.entries(raw)) {
16
+ const { targetPath, package: packageName, ...rest } = entry
17
+ if (!targetPath) fail(`Built-in target '${id}' is missing targetPath in ${file}.`, ExitCode.missingDependency)
18
+ const packageRoot = packageName ? packageDirFor(ctx, packageName) : ctx.targetsRoot
19
+ out[id] = {
20
+ ...rest,
21
+ id,
22
+ package: packageName || '@geastack/targets',
23
+ targetDir: packageRoot ? path.join(packageRoot, targetPath) : ''
24
+ }
25
+ }
26
+ return out
27
+ }
28
+
29
+ export function requireTargets(ctx) {
30
+ if (!ctx.targetsRoot) {
31
+ fail('@geastack/targets is not installed in this project (npm i @geastack/targets).', ExitCode.missingDependency)
32
+ }
33
+ const targets = loadTargets(ctx)
34
+ if (Object.keys(targets).length === 0) {
35
+ fail(`No targets.json found in ${ctx.targetsRoot}.`, ExitCode.missingDependency)
36
+ }
37
+ return targets
38
+ }
39
+
40
+ function packageDirFor(ctx, packageName) {
41
+ for (const field of Object.keys(ctx)) {
42
+ if (typeof ctx.packageName === 'function' && ctx.packageName(field) === packageName) return ctx[field]
43
+ }
44
+ return ''
45
+ }