@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.
- package/README.md +14 -1
- package/docs/ESP32-WAVESHARE-AMOLED-QUICKSTART.md +2 -2
- package/docs/NPX-COMMANDS.md +3 -1
- package/docs/SETUP.md +81 -15
- package/docs/SPEC.md +8 -5
- package/package.json +2 -3
- package/src/apps/app-index-writer.mjs +15 -0
- package/src/apps/apple-icons.mjs +147 -0
- package/src/apps/bundle-writer.mjs +156 -0
- package/src/apps/launcher-catalog.mjs +114 -0
- package/src/apps/openai-icons.mjs +356 -0
- package/src/apps/zip-writer.mjs +104 -0
- package/src/ble/ble-ota.swift +291 -0
- package/src/boards/config.mjs +114 -0
- package/src/boards/custom-target.mjs +309 -0
- package/src/boards/resolve.mjs +145 -0
- package/src/boards/targets.mjs +45 -0
- package/src/boards/usb.mjs +252 -0
- package/src/chips.mjs +1 -1
- package/src/commands/apps.mjs +204 -0
- package/src/commands/board.mjs +402 -0
- package/src/commands/boards.mjs +334 -0
- package/src/commands/doctor.mjs +91 -0
- package/src/context.mjs +50 -54
- package/src/device/device.mjs +96 -0
- package/src/device/image.mjs +115 -0
- package/src/device/serial.mjs +430 -0
- package/src/device/wifi.mjs +190 -0
- package/src/esp32/build.mjs +321 -0
- package/src/esp32/capabilities.mjs +54 -0
- package/src/esp32/flash.mjs +181 -0
- package/src/esp32/idf-env.mjs +171 -0
- package/src/esp32/ota.mjs +80 -0
- package/src/esp32/partitions.mjs +59 -0
- package/src/esp32/sdkconfig.mjs +103 -0
- package/src/esp32/wifi-config.mjs +72 -0
- package/src/gea.mjs +89 -427
- package/src/geaos/adapter.mjs +119 -0
- package/src/heap-report.mjs +1276 -0
- package/src/manifest.mjs +135 -36
- package/src/rp2350/adapter.mjs +155 -0
- package/src/serial-devices.mjs +32 -20
- package/src/setup-wizard.mjs +11 -12
- package/src/taurus/adapter.mjs +52 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, readdirSync, realpathSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
// A board is identified by its USB SERIAL, never by a /dev path: enumerated
|
|
6
|
+
// paths change on every plug-in and identical boards routinely share one.
|
|
7
|
+
// Everything here maps a stable serial to whatever port the OS gave it today.
|
|
8
|
+
|
|
9
|
+
const serialPatterns = [
|
|
10
|
+
/^cu\.usbmodem/,
|
|
11
|
+
/^tty\.usbmodem/,
|
|
12
|
+
/^cu\.usbserial/,
|
|
13
|
+
/^tty\.usbserial/,
|
|
14
|
+
/^cu\.SLAB_USBtoUART/,
|
|
15
|
+
/^tty\.SLAB_USBtoUART/,
|
|
16
|
+
/^ttyACM/,
|
|
17
|
+
/^ttyUSB/
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export function serialPortCandidates() {
|
|
21
|
+
const dev = '/dev'
|
|
22
|
+
if (!existsSync(dev)) return []
|
|
23
|
+
return readdirSync(dev)
|
|
24
|
+
.filter((name) => serialPatterns.some((pattern) => pattern.test(name)))
|
|
25
|
+
.map((name) => path.join(dev, name))
|
|
26
|
+
.sort()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveAutoUsbPort() {
|
|
30
|
+
const candidates = serialPortCandidates()
|
|
31
|
+
const callout = candidates.filter((candidate) => path.basename(candidate).startsWith('cu.'))
|
|
32
|
+
const usable = callout.length > 0 ? callout : candidates
|
|
33
|
+
if (usable.length <= 1) return usable[0] || ''
|
|
34
|
+
throw new Error(
|
|
35
|
+
['Multiple USB serial ports are present; use --board <alias> with a registered USB serial.', ...usable.map((c) => ` ${c}`)].join('\n')
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizedSerial(value) {
|
|
40
|
+
return String(value || '').replace(/[^a-z0-9]/gi, '').toLowerCase()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function linuxSerialByIdCandidates(serial) {
|
|
44
|
+
const dir = '/dev/serial/by-id'
|
|
45
|
+
if (!existsSync(dir)) return []
|
|
46
|
+
const needle = normalizedSerial(serial)
|
|
47
|
+
return readdirSync(dir)
|
|
48
|
+
.filter((name) => normalizedSerial(name).includes(needle))
|
|
49
|
+
.map((name) => path.join(dir, name))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ioregStringProperty(line, key) {
|
|
53
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
54
|
+
return line.match(new RegExp(`"${escaped}"\\s*=\\s*"([^"]*)"`))?.[1] || ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ioregNumberProperty(line, key) {
|
|
58
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
59
|
+
const value = line.match(new RegExp(`"${escaped}"\\s*=\\s*(\\d+)`))?.[1] || ''
|
|
60
|
+
return value ? Number(value) : 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function macUsbDeviceForSerial(serial, ioreg = runIoreg) {
|
|
64
|
+
const output = ioreg(['-p', 'IOUSB', '-l', '-w0'])
|
|
65
|
+
let current = null
|
|
66
|
+
const devices = []
|
|
67
|
+
for (const line of output.split(/\r?\n/)) {
|
|
68
|
+
// One `| ` per nesting level -- devices behind hubs carry several.
|
|
69
|
+
const node = line.match(/^([\s|]*)[+\\-]*o .*@([0-9a-fA-F]+)\s+<class IOUSBHostDevice/)
|
|
70
|
+
if (node) {
|
|
71
|
+
if (current) devices.push(current)
|
|
72
|
+
current = { locationHex: node[2], text: line }
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
if (current) current.text += `\n${line}`
|
|
76
|
+
}
|
|
77
|
+
if (current) devices.push(current)
|
|
78
|
+
const needle = normalizedSerial(serial)
|
|
79
|
+
return devices.find((device) =>
|
|
80
|
+
device.text.split(/\r?\n/).some((line) => {
|
|
81
|
+
const value = ioregStringProperty(line, 'kUSBSerialNumberString') || ioregStringProperty(line, 'USB Serial Number')
|
|
82
|
+
return value && normalizedSerial(value) === needle
|
|
83
|
+
})
|
|
84
|
+
) || null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function locationDigits(locationHex) {
|
|
88
|
+
return locationHex.toLowerCase().replace(/^0+/, '').replace(/0+$/, '').replace(/[^0-9a-f]/g, '')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function macUsbCalloutPortsForSerial(serial, ioreg = runIoreg) {
|
|
92
|
+
const output = ioreg(['-p', 'IOService', '-l', '-w0'], { maxBuffer: 64 * 1024 * 1024 })
|
|
93
|
+
const stack = []
|
|
94
|
+
const matches = new Set()
|
|
95
|
+
const needle = normalizedSerial(serial)
|
|
96
|
+
for (const line of output.split(/\r?\n/)) {
|
|
97
|
+
const node = line.match(/^([\s|]*)[+\\-]*o\s+/)
|
|
98
|
+
if (node) {
|
|
99
|
+
const depth = (node[1].match(/\|/g) || []).length
|
|
100
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) stack.pop()
|
|
101
|
+
stack.push({ depth, serial: '' })
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (stack.length === 0) continue
|
|
105
|
+
const current = stack[stack.length - 1]
|
|
106
|
+
const serialValue = ioregStringProperty(line, 'kUSBSerialNumberString') || ioregStringProperty(line, 'USB Serial Number')
|
|
107
|
+
if (serialValue) current.serial = serialValue
|
|
108
|
+
const callout = ioregStringProperty(line, 'IOCalloutDevice')
|
|
109
|
+
if (!callout || !path.basename(callout).startsWith('cu.')) continue
|
|
110
|
+
const inherited = [...stack].reverse().find((entry) => entry.serial)?.serial || ''
|
|
111
|
+
if (normalizedSerial(inherited) === needle) matches.add(callout)
|
|
112
|
+
}
|
|
113
|
+
return [...matches].sort()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Every USB callout port the registry knows, with the serial and product
|
|
117
|
+
// name inherited from the enclosing USB device: the inverse of
|
|
118
|
+
// macUsbCalloutPortsForSerial, for `gea boards discover` and the setup
|
|
119
|
+
// wizard. The /dev name carries no identity (the usbmodem number changes on
|
|
120
|
+
// every enumeration and identical boards share it), so this is the only
|
|
121
|
+
// place a port's serial can come from on macOS.
|
|
122
|
+
export function listMacUsbCalloutPorts(ioreg = runIoreg) {
|
|
123
|
+
let output = ''
|
|
124
|
+
try {
|
|
125
|
+
output = ioreg(['-p', 'IOService', '-l', '-w0'], { maxBuffer: 64 * 1024 * 1024 })
|
|
126
|
+
} catch {
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
const stack = []
|
|
130
|
+
const ports = new Map()
|
|
131
|
+
for (const line of output.split(/\r?\n/)) {
|
|
132
|
+
const node = line.match(/^([\s|]*)[+\\-]*o\s+/)
|
|
133
|
+
if (node) {
|
|
134
|
+
const depth = (node[1].match(/\|/g) || []).length
|
|
135
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) stack.pop()
|
|
136
|
+
stack.push({ depth, serial: '', product: '' })
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if (stack.length === 0) continue
|
|
140
|
+
const current = stack[stack.length - 1]
|
|
141
|
+
const serialValue = ioregStringProperty(line, 'kUSBSerialNumberString') || ioregStringProperty(line, 'USB Serial Number')
|
|
142
|
+
if (serialValue) current.serial = serialValue
|
|
143
|
+
const productValue = ioregStringProperty(line, 'kUSBProductString') || ioregStringProperty(line, 'USB Product Name')
|
|
144
|
+
if (productValue) current.product = productValue
|
|
145
|
+
const callout = ioregStringProperty(line, 'IOCalloutDevice')
|
|
146
|
+
if (!callout || !path.basename(callout).startsWith('cu.')) continue
|
|
147
|
+
// A callout with no USB serial above it is not a USB device at all
|
|
148
|
+
// (Bluetooth SPP, the debug console); probing those is a wasted timeout.
|
|
149
|
+
const serial = [...stack].reverse().find((entry) => entry.serial)?.serial || ''
|
|
150
|
+
if (!serial) continue
|
|
151
|
+
const label = [...stack].reverse().find((entry) => entry.product)?.product || ''
|
|
152
|
+
ports.set(callout, { path: callout, serial, label })
|
|
153
|
+
}
|
|
154
|
+
return [...ports.values()].sort((a, b) => a.path.localeCompare(b.path))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function runIoreg(args, options = {}) {
|
|
158
|
+
return execFileSync('ioreg', args, { encoding: 'utf8', ...options })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function resolveMacUsbSerialPort(serial, ioreg = runIoreg) {
|
|
162
|
+
const candidates = serialPortCandidates().filter((candidate) => path.basename(candidate).startsWith('cu.'))
|
|
163
|
+
const needle = normalizedSerial(serial)
|
|
164
|
+
const serialMatches = candidates.filter((candidate) => normalizedSerial(path.basename(candidate)).includes(needle))
|
|
165
|
+
if (serialMatches.length === 1) return serialMatches[0]
|
|
166
|
+
|
|
167
|
+
const location = macUsbDeviceForSerial(serial, ioreg)?.locationHex || ''
|
|
168
|
+
const digits = locationDigits(location)
|
|
169
|
+
if (digits) {
|
|
170
|
+
const locationMatches = candidates.filter((candidate) => normalizedSerial(path.basename(candidate)).includes(digits))
|
|
171
|
+
if (locationMatches.length === 1) return locationMatches[0]
|
|
172
|
+
const registryMatches = macUsbCalloutPortsForSerial(serial, ioreg)
|
|
173
|
+
if (registryMatches.length === 1) return registryMatches[0]
|
|
174
|
+
if (registryMatches.length > 1) {
|
|
175
|
+
throw new Error([`USB serial ${serial} maps to multiple /dev/cu.* ports:`, ...registryMatches.map((c) => ` ${c}`)].join('\n'))
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`Could not map USB serial ${serial} to a /dev/cu.* port. Check that the board is attached.`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function resolveUsbSerialPort({ serial }, { platform = process.platform, ioreg = runIoreg } = {}) {
|
|
182
|
+
if (!serial) throw new Error('A USB serial number is required to locate the board; /dev paths are not accepted.')
|
|
183
|
+
if (platform === 'linux') {
|
|
184
|
+
const matches = linuxSerialByIdCandidates(serial)
|
|
185
|
+
if (matches.length === 1) return realpathSync(matches[0])
|
|
186
|
+
throw new Error(`Could not map USB serial ${serial} to a /dev/serial/by-id entry. Check that the board is attached.`)
|
|
187
|
+
}
|
|
188
|
+
if (platform === 'darwin') return resolveMacUsbSerialPort(serial, ioreg)
|
|
189
|
+
throw new Error(`USB serial lookup is not implemented on ${platform}.`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// picotool selects by bus/address on macOS (its --ser matching is unreliable
|
|
193
|
+
// there); everywhere else the serial itself is the selector.
|
|
194
|
+
export function resolvePicotoolSelection({ serial }, { platform = process.platform, ioreg = runIoreg } = {}) {
|
|
195
|
+
if (!serial) return []
|
|
196
|
+
if (platform !== 'darwin') return ['--ser', serial]
|
|
197
|
+
let device = null
|
|
198
|
+
try {
|
|
199
|
+
device = macUsbDeviceForSerial(serial, ioreg)
|
|
200
|
+
} catch {
|
|
201
|
+
device = null
|
|
202
|
+
}
|
|
203
|
+
if (!device?.locationHex) return ['--ser', serial]
|
|
204
|
+
const bus = Number.parseInt(device.locationHex.slice(0, 2), 16)
|
|
205
|
+
const address = device.text
|
|
206
|
+
.split(/\r?\n/)
|
|
207
|
+
.map((line) => ioregNumberProperty(line, 'USB Address') || ioregNumberProperty(line, 'kUSBAddress'))
|
|
208
|
+
.find(Boolean) || 0
|
|
209
|
+
if (!bus || !address) return ['--ser', serial]
|
|
210
|
+
return ['--bus', String(bus), '--address', String(address)]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Poll until the board's port exists. A board reboots (and re-enumerates)
|
|
214
|
+
// after a flash, so callers retry on the SERIAL, not on a cached path.
|
|
215
|
+
export async function waitForSerialPort({
|
|
216
|
+
port = '',
|
|
217
|
+
serial = '',
|
|
218
|
+
timeoutSeconds = 300,
|
|
219
|
+
label = 'USB serial port',
|
|
220
|
+
pollSeconds = 1,
|
|
221
|
+
log = (line) => process.stderr.write(`${line}\n`),
|
|
222
|
+
resolver = resolveUsbSerialPort
|
|
223
|
+
} = {}) {
|
|
224
|
+
const startedAt = Date.now()
|
|
225
|
+
let nextLog = startedAt
|
|
226
|
+
while (true) {
|
|
227
|
+
if (port) {
|
|
228
|
+
if (existsSync(port)) return port
|
|
229
|
+
} else if (serial) {
|
|
230
|
+
try {
|
|
231
|
+
const resolved = resolver({ serial })
|
|
232
|
+
if (resolved && existsSync(resolved)) return resolved
|
|
233
|
+
} catch {
|
|
234
|
+
// not attached yet
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
const candidate = serialPortCandidates()[0]
|
|
238
|
+
if (candidate) return candidate
|
|
239
|
+
}
|
|
240
|
+
const elapsed = (Date.now() - startedAt) / 1000
|
|
241
|
+
if (timeoutSeconds > 0 && elapsed >= timeoutSeconds) {
|
|
242
|
+
const where = port ? ` at ${port}` : serial ? ` with USB serial ${serial}` : ''
|
|
243
|
+
throw new Error(`Timed out waiting for ${label}${where}.`)
|
|
244
|
+
}
|
|
245
|
+
if (Date.now() >= nextLog) {
|
|
246
|
+
const where = port ? ` at ${port}` : serial ? ` with USB serial ${serial}` : ''
|
|
247
|
+
log(`Waiting for ${label}${where}...`)
|
|
248
|
+
nextLog = Date.now() + 5000
|
|
249
|
+
}
|
|
250
|
+
await new Promise((resolve) => setTimeout(resolve, pollSeconds * 1000))
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/chips.mjs
CHANGED
|
@@ -142,7 +142,7 @@ export function resolveTargetDefinition(ctx, parsed) {
|
|
|
142
142
|
const configPath = option(parsed, 'boards-config')
|
|
143
143
|
? path.resolve(ctx.cwd, option(parsed, 'boards-config'))
|
|
144
144
|
: ctx.projectBoardsConfig
|
|
145
|
-
if (!configPath || !exists(configPath)) fail(
|
|
145
|
+
if (!configPath || !exists(configPath)) fail(`No board config was found at ${configPath || '.gea/boards.json'}. Run gea setup and create a custom board first.`, ExitCode.usage)
|
|
146
146
|
const boards = readJson(configPath)
|
|
147
147
|
const requested = option(parsed, 'board', '')
|
|
148
148
|
const customBoards = Object.entries(boards).filter(([, entry]) => typeof entry?.targetDefinition === 'string' && entry.targetDefinition)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { flag, option, optionList } from '../args.mjs'
|
|
5
|
+
import { loadTargets } from '../boards/targets.mjs'
|
|
6
|
+
import { ExitCode, fail } from '../errors.mjs'
|
|
7
|
+
import { exists } from '../fs-utils.mjs'
|
|
8
|
+
import { appCmakeMeta, appSummary, assertValidApp, discoverApps, resolveRequestedApp } from '../manifest.mjs'
|
|
9
|
+
|
|
10
|
+
// gea apps ... : the app catalog and everything generated from it.
|
|
11
|
+
|
|
12
|
+
export async function appsCommand(ctx, parsed, rest, options) {
|
|
13
|
+
const sub = rest[0] || 'list'
|
|
14
|
+
const args = rest.slice(1)
|
|
15
|
+
switch (sub) {
|
|
16
|
+
case 'list': return listApps(ctx, parsed, options)
|
|
17
|
+
case 'inspect': return inspectApp(ctx, parsed, args, options)
|
|
18
|
+
case 'pack': return packApp(ctx, parsed, args, options)
|
|
19
|
+
case 'index': return appIndex(ctx, parsed, options)
|
|
20
|
+
case 'launcher': return launcher(ctx, parsed, options)
|
|
21
|
+
case 'icons': return icons(ctx, parsed, args, options)
|
|
22
|
+
case 'icon-sheet': return iconSheet(ctx, parsed, options)
|
|
23
|
+
case 'apple-icons': return appleIcons(ctx, parsed, args, options)
|
|
24
|
+
default:
|
|
25
|
+
fail(`Unknown apps subcommand '${sub}'. Expected list, inspect, pack, index, launcher, icons, icon-sheet, or apple-icons.`, ExitCode.usage)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listApps(ctx, parsed, options) {
|
|
30
|
+
const target = option(parsed, 'target', '')
|
|
31
|
+
const apps = discoverApps(ctx).filter((app) => !target || app.targets?.[target] === true)
|
|
32
|
+
if (flag(parsed, 'json')) options.stdout(JSON.stringify(apps.map((app) => appSummary(ctx, app)), null, 2))
|
|
33
|
+
else for (const app of apps) options.stdout(app.id)
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function inspectApp(ctx, parsed, args, options) {
|
|
38
|
+
const app = resolveRequestedApp(ctx, parsed, args)
|
|
39
|
+
assertValidApp(app)
|
|
40
|
+
const format = option(parsed, 'format', flag(parsed, 'json') ? 'json' : 'text')
|
|
41
|
+
const summary = appSummary(ctx, app)
|
|
42
|
+
if (format === 'cmake') options.stdout(appCmakeMeta(ctx, app))
|
|
43
|
+
else if (format === 'shell') options.stdout(`${summary.root}\t${app.entry}\t${app.runtime}\t${app.name}`)
|
|
44
|
+
else if (format === 'json') options.stdout(JSON.stringify(summary, null, 2))
|
|
45
|
+
else options.stdout(`${app.id}\t${app.name}\t${summary.root}\t${app.entry}`)
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function packApp(ctx, parsed, args, options) {
|
|
50
|
+
const app = resolveRequestedApp(ctx, parsed, args)
|
|
51
|
+
assertValidApp(app)
|
|
52
|
+
const target = option(parsed, 'target', 'geaos')
|
|
53
|
+
const { createGeaBundle } = await import('../apps/bundle-writer.mjs')
|
|
54
|
+
const out = path.resolve(ctx.cwd, option(parsed, 'out', path.join(ctx.projectRoot, 'dist', `${app.id}-${target}.gea.zip`)))
|
|
55
|
+
const artifactDir = option(parsed, 'artifact-dir', '')
|
|
56
|
+
const manifest = createGeaBundle({ repoRoot: ctx.projectRoot, app, target, outputPath: out, artifactDir: artifactDir ? path.resolve(ctx.cwd, artifactDir) : '' })
|
|
57
|
+
options.stdout(flag(parsed, 'json') ? JSON.stringify({ outputPath: out, manifest }, null, 2) : out)
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function appIndex(ctx, parsed, options) {
|
|
62
|
+
const { generateAppIndex } = await import('../apps/app-index-writer.mjs')
|
|
63
|
+
const outputFile = path.resolve(ctx.cwd, option(parsed, 'out', path.join(ctx.projectRoot, 'simulator', 'src', 'generated', 'app-index.ts')))
|
|
64
|
+
const apps = generateAppIndex({ apps: discoverApps(ctx).map((app) => appSummary(ctx, app)), outputFile })
|
|
65
|
+
options.stdout(flag(parsed, 'json') ? JSON.stringify({ outputFile, apps }, null, 2) : outputFile)
|
|
66
|
+
return 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function launcher(ctx, parsed, options) {
|
|
70
|
+
const { generateLauncherCatalog } = await import('../apps/launcher-catalog.mjs')
|
|
71
|
+
const target = option(parsed, 'target', 'geaos')
|
|
72
|
+
const outputFile = path.resolve(ctx.cwd, option(parsed, 'out', path.join(ctx.projectRoot, 'apps', 'app-launcher', 'generated', 'LauncherCatalog.tsx')))
|
|
73
|
+
const apps = generateLauncherCatalog({ repoRoot: ctx.projectRoot, apps: discoverApps(ctx), target, outputFile })
|
|
74
|
+
options.stdout(flag(parsed, 'json') ? JSON.stringify({ outputFile, apps: apps.map((app) => appSummary(ctx, app)) }, null, 2) : outputFile)
|
|
75
|
+
return 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function icons(ctx, parsed, args, options) {
|
|
79
|
+
const requested = args[0] || option(parsed, 'app', '')
|
|
80
|
+
if (!requested) fail('gea apps icons <app-id|all> [--target <t>] [--model <m>] [--concurrency <n>] [--exclude a,b]', ExitCode.usage)
|
|
81
|
+
const { generateIconSets } = await import('../apps/openai-icons.mjs')
|
|
82
|
+
const excluded = new Set(optionList(parsed, 'exclude'))
|
|
83
|
+
const iconTarget = option(parsed, 'target', '')
|
|
84
|
+
const all = discoverApps(ctx)
|
|
85
|
+
const apps = requested === 'all'
|
|
86
|
+
? all
|
|
87
|
+
.filter((app) => iconTarget && iconTarget !== 'all'
|
|
88
|
+
? app.targets?.[iconTarget] === true
|
|
89
|
+
: ['geaos', 'esp32', 'ios', 'macos'].some((candidate) => app.targets?.[candidate] === true))
|
|
90
|
+
.filter((app) => !excluded.has(app.id))
|
|
91
|
+
: [resolveRequestedApp(ctx, parsed, [requested])]
|
|
92
|
+
await generateIconSets({
|
|
93
|
+
repoRoot: ctx.projectRoot,
|
|
94
|
+
apps,
|
|
95
|
+
model: option(parsed, 'model', ctx.env.GEA_OPENAI_IMAGE_MODEL || 'gpt-image-2'),
|
|
96
|
+
concurrency: Math.max(1, Number.parseInt(option(parsed, 'concurrency', '1'), 10) || 1)
|
|
97
|
+
})
|
|
98
|
+
return 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The Apple build scripts call this per app: the iOS generator wants an
|
|
102
|
+
// AppIcon.appiconset inside its asset catalog, the macOS bundle wants an
|
|
103
|
+
// AppIcon.icns in Resources. --signature prints a digest of every input the
|
|
104
|
+
// icon depends on so the scripts can skip regeneration when nothing changed.
|
|
105
|
+
async function appleIcons(ctx, parsed, args, options) {
|
|
106
|
+
const usage = 'gea apps apple-icons <app-id> --platform ios --assets-dir <dir> | --platform macos --build-dir <dir> --resources-dir <dir> [--signature]'
|
|
107
|
+
const app = resolveRequestedApp(ctx, parsed, args)
|
|
108
|
+
assertValidApp(app)
|
|
109
|
+
const platform = option(parsed, 'platform', '')
|
|
110
|
+
if (platform !== 'ios' && platform !== 'macos') fail(usage, ExitCode.usage)
|
|
111
|
+
const icons = await import('../apps/apple-icons.mjs')
|
|
112
|
+
const repoRoot = ctx.projectRoot
|
|
113
|
+
if (flag(parsed, 'signature')) {
|
|
114
|
+
const { createHash } = await import('node:crypto')
|
|
115
|
+
const { readFileSync } = await import('node:fs')
|
|
116
|
+
const hash = createHash('sha256')
|
|
117
|
+
const addFile = (file) => {
|
|
118
|
+
hash.update(file)
|
|
119
|
+
hash.update('\0')
|
|
120
|
+
hash.update(readFileSync(file))
|
|
121
|
+
hash.update('\0')
|
|
122
|
+
}
|
|
123
|
+
addFile(path.join(ctx.cliPackageRoot, 'src', 'apps', 'apple-icons.mjs'))
|
|
124
|
+
hash.update(JSON.stringify({ id: app.id, root: appSummary(ctx, app).root, icons: app.icons || {}, platform }))
|
|
125
|
+
addFile(icons.resolveAppleIconSourcePath(repoRoot, app))
|
|
126
|
+
options.stdout(hash.digest('hex'))
|
|
127
|
+
return 0
|
|
128
|
+
}
|
|
129
|
+
if (platform === 'ios') {
|
|
130
|
+
const assetsDir = option(parsed, 'assets-dir', '')
|
|
131
|
+
if (!assetsDir) fail(usage, ExitCode.usage)
|
|
132
|
+
options.stdout(icons.prepareIosAppIconAssets({ repoRoot, app, assetsDir: path.resolve(ctx.cwd, assetsDir) }))
|
|
133
|
+
return 0
|
|
134
|
+
}
|
|
135
|
+
const buildDir = option(parsed, 'build-dir', '')
|
|
136
|
+
const resourcesDir = option(parsed, 'resources-dir', '')
|
|
137
|
+
if (!buildDir || !resourcesDir) fail(usage, ExitCode.usage)
|
|
138
|
+
options.stdout(icons.prepareMacosAppIcon({ repoRoot, app, buildDir: path.resolve(ctx.cwd, buildDir), resourcesDir: path.resolve(ctx.cwd, resourcesDir) }))
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function iconSheet(ctx, parsed, options) {
|
|
143
|
+
const { generateIconStyleSheet } = await import('../apps/openai-icons.mjs')
|
|
144
|
+
const excluded = new Set(optionList(parsed, 'exclude'))
|
|
145
|
+
const iconTarget = option(parsed, 'target', 'geaos')
|
|
146
|
+
const outputPath = path.resolve(ctx.cwd, option(parsed, 'out', path.join(ctx.projectRoot, 'docs', 'icon-style-preview', `${iconTarget}-icon-style-sheet.png`)))
|
|
147
|
+
const apps = discoverApps(ctx)
|
|
148
|
+
.filter((app) => iconTarget === 'all' || app.targets?.[iconTarget] === true)
|
|
149
|
+
.filter((app) => flag(parsed, 'include-hidden') || !app.launcher.hidden)
|
|
150
|
+
.filter((app) => app.icons && Object.keys(app.icons).length > 0)
|
|
151
|
+
.filter((app) => !excluded.has(app.id))
|
|
152
|
+
if (apps.length === 0) fail(`No apps matched target '${iconTarget}'`, ExitCode.usage)
|
|
153
|
+
const sheet = await generateIconStyleSheet({
|
|
154
|
+
apps,
|
|
155
|
+
model: option(parsed, 'model', ctx.env.GEA_OPENAI_IMAGE_MODEL || 'gpt-image-2'),
|
|
156
|
+
outputPath,
|
|
157
|
+
columns: Math.max(1, Number.parseInt(option(parsed, 'columns', '5'), 10) || 5)
|
|
158
|
+
})
|
|
159
|
+
options.stdout(flag(parsed, 'json') ? JSON.stringify({ outputPath: sheet.outputPath, apps: apps.map((app) => appSummary(ctx, app)) }, null, 2) : sheet.outputPath)
|
|
160
|
+
return 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// gea targets ...
|
|
164
|
+
|
|
165
|
+
export function targetsCommand(ctx, parsed, rest, options) {
|
|
166
|
+
const sub = rest[0] || 'list'
|
|
167
|
+
let targets
|
|
168
|
+
try {
|
|
169
|
+
targets = loadTargets(ctx)
|
|
170
|
+
} catch (error) {
|
|
171
|
+
fail(error.message, ExitCode.missingDependency)
|
|
172
|
+
}
|
|
173
|
+
if (sub === 'list') {
|
|
174
|
+
if (flag(parsed, 'json')) options.stdout(JSON.stringify(targets, null, 2))
|
|
175
|
+
else for (const id of Object.keys(targets).sort()) options.stdout(`${id}\t${targets[id].adapter}\t${exists(targets[id].targetDir) ? targets[id].targetDir : '(not installed)'}`)
|
|
176
|
+
return 0
|
|
177
|
+
}
|
|
178
|
+
if (sub === 'show') {
|
|
179
|
+
const id = rest[1] || ''
|
|
180
|
+
if (!targets[id]) fail(`Unknown target '${id}'. Run gea targets list.`, ExitCode.usage)
|
|
181
|
+
options.stdout(JSON.stringify(targets[id], null, 2))
|
|
182
|
+
return 0
|
|
183
|
+
}
|
|
184
|
+
fail(`Unknown targets subcommand '${sub}'. Expected list or show.`, ExitCode.usage)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function heapReportCommand(ctx, parsed, rest, options) {
|
|
188
|
+
const { DEFAULT_TITLE, parseLogs, renderHtml } = await import('../heap-report.mjs')
|
|
189
|
+
const { mkdirSync, writeFileSync } = await import('node:fs')
|
|
190
|
+
let inputs = rest.map((file) => path.resolve(ctx.cwd, file))
|
|
191
|
+
const logsDir = path.join(ctx.projectRoot, 'docs', 'esp32-perf-logs')
|
|
192
|
+
if (inputs.length === 0 && exists(logsDir)) {
|
|
193
|
+
inputs = readdirSync(logsDir).filter((name) => /\.(txt|log)$/.test(name)).map((name) => path.join(logsDir, name))
|
|
194
|
+
}
|
|
195
|
+
if (inputs.length === 0) fail('No heap log inputs found. Pass log files or place .txt/.log files in docs/esp32-perf-logs/.', ExitCode.usage)
|
|
196
|
+
const out = path.resolve(ctx.cwd, option(parsed, 'out', path.join(logsDir, 'heap-map-report.html')))
|
|
197
|
+
const maps = optionList(parsed, 'map').map((file) => path.resolve(ctx.cwd, file))
|
|
198
|
+
const data = parseLogs(inputs, maps)
|
|
199
|
+
mkdirSync(path.dirname(out), { recursive: true })
|
|
200
|
+
writeFileSync(out, renderHtml(data, option(parsed, 'title', DEFAULT_TITLE)))
|
|
201
|
+
options.stdout(`Wrote heap map report: ${out}`)
|
|
202
|
+
options.stdout(`Parsed ${data.snapshots.length} heap-map snapshots, ${data.probes.length} probes, ${data.liveAllocations.length} live allocations, ${data.linkerMaps.length} linker map(s)`)
|
|
203
|
+
return 0
|
|
204
|
+
}
|