@geastack/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +161 -0
- package/bin/create-geastack.mjs +15 -0
- package/bin/gea.mjs +15 -0
- package/docs/ESP32-WAVESHARE-AMOLED-QUICKSTART.md +133 -0
- package/docs/NPX-COMMANDS.md +144 -0
- package/docs/SETUP.md +276 -0
- package/docs/SPEC.md +217 -0
- package/examples/catalog.json +668 -0
- package/package.json +41 -0
- package/src/args.mjs +78 -0
- package/src/board-catalog.mjs +224 -0
- package/src/context.mjs +110 -0
- package/src/create-geastack.mjs +430 -0
- package/src/errors.mjs +20 -0
- package/src/fs-utils.mjs +47 -0
- package/src/gea.mjs +444 -0
- package/src/manifest.mjs +171 -0
- package/src/prompts.mjs +60 -0
- package/src/run.mjs +33 -0
- package/src/serial-devices.mjs +143 -0
- package/src/setup-wizard.mjs +633 -0
- package/src/starter-catalog.mjs +191 -0
- package/src/toolchain.mjs +15 -0
- package/starters/bundled/counter/index.tsx +25 -0
- package/starters/bundled/counter/package.json +33 -0
- package/starters/bundled/counter/store.ts +7 -0
- package/starters/bundled/counter/styles.css +39 -0
- package/starters/catalog.json +22 -0
package/src/run.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import { CliError, ExitCode } from './errors.mjs'
|
|
4
|
+
|
|
5
|
+
export function runExternal(command, args, options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
cwd = process.cwd(),
|
|
8
|
+
env = process.env,
|
|
9
|
+
dryRun = false,
|
|
10
|
+
failureCode = ExitCode.generic,
|
|
11
|
+
stdout = console.log
|
|
12
|
+
} = options
|
|
13
|
+
if (dryRun) {
|
|
14
|
+
stdout(formatCommand([command, ...args]))
|
|
15
|
+
return 0
|
|
16
|
+
}
|
|
17
|
+
const result = spawnSync(command, args, { cwd, env, stdio: 'inherit' })
|
|
18
|
+
if (result.error) throw result.error
|
|
19
|
+
if (result.status !== 0) {
|
|
20
|
+
throw new CliError(`ERROR: Command failed (${result.status ?? 1}): ${formatCommand([command, ...args])}`, failureCode)
|
|
21
|
+
}
|
|
22
|
+
return result.status ?? 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatCommand(parts) {
|
|
26
|
+
return parts.map(shellQuote).join(' ')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function shellQuote(value) {
|
|
30
|
+
const text = String(value)
|
|
31
|
+
if (/^[A-Za-z0-9_/:=.,@%+\-]+$/.test(text)) return text
|
|
32
|
+
return `'${text.replaceAll("'", "'\\''")}'`
|
|
33
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
export function detectSerialDevices({ env = process.env, platform = process.platform } = {}) {
|
|
6
|
+
if (env.GEA_SERIAL_DEVICES) return parseSerialDevices(env.GEA_SERIAL_DEVICES)
|
|
7
|
+
if (platform === 'win32') return detectWindowsSerialDevices(env)
|
|
8
|
+
return detectUnixSerialDevices()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatSerialDevice(device) {
|
|
12
|
+
const bits = [device.path]
|
|
13
|
+
if (device.label && device.label !== device.path) bits.push(device.label)
|
|
14
|
+
if (device.serial) bits.push(`serial ${device.serial}`)
|
|
15
|
+
return bits.join(' - ')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseSerialDevices(value) {
|
|
19
|
+
const text = String(value || '').trim()
|
|
20
|
+
if (!text) return []
|
|
21
|
+
if (text.startsWith('[')) {
|
|
22
|
+
return JSON.parse(text).map(normalizeDevice).filter((device) => device.path)
|
|
23
|
+
}
|
|
24
|
+
return text
|
|
25
|
+
.split(/[,\n]/)
|
|
26
|
+
.map((entry) => entry.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.map((entry) => {
|
|
29
|
+
const [devicePath, label = '', serial = ''] = entry.split('|').map((part) => part.trim())
|
|
30
|
+
return normalizeDevice({ path: devicePath, label, serial })
|
|
31
|
+
})
|
|
32
|
+
.filter((device) => device.path)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function detectUnixSerialDevices() {
|
|
36
|
+
const devices = new Map()
|
|
37
|
+
addLinuxByIdDevices(devices)
|
|
38
|
+
addDevPatternDevices(devices)
|
|
39
|
+
return [...devices.values()].sort((a, b) => a.path.localeCompare(b.path))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addLinuxByIdDevices(devices) {
|
|
43
|
+
const byId = '/dev/serial/by-id'
|
|
44
|
+
if (!isDirectory(byId)) return
|
|
45
|
+
for (const name of safeReaddir(byId)) {
|
|
46
|
+
const devicePath = path.join(byId, name)
|
|
47
|
+
devices.set(devicePath, normalizeDevice({
|
|
48
|
+
path: devicePath,
|
|
49
|
+
label: name,
|
|
50
|
+
serial: serialFromLinuxById(name)
|
|
51
|
+
}))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function addDevPatternDevices(devices) {
|
|
56
|
+
const dev = '/dev'
|
|
57
|
+
if (!isDirectory(dev)) return
|
|
58
|
+
const patterns = [
|
|
59
|
+
/^cu\.usbmodem/,
|
|
60
|
+
/^tty\.usbmodem/,
|
|
61
|
+
/^cu\.usbserial/,
|
|
62
|
+
/^tty\.usbserial/,
|
|
63
|
+
/^cu\.SLAB_USBtoUART/,
|
|
64
|
+
/^tty\.SLAB_USBtoUART/,
|
|
65
|
+
/^ttyACM/,
|
|
66
|
+
/^ttyUSB/
|
|
67
|
+
]
|
|
68
|
+
for (const name of safeReaddir(dev)) {
|
|
69
|
+
if (!patterns.some((pattern) => pattern.test(name))) continue
|
|
70
|
+
const devicePath = path.join(dev, name)
|
|
71
|
+
devices.set(devicePath, normalizeDevice({
|
|
72
|
+
path: devicePath,
|
|
73
|
+
label: name,
|
|
74
|
+
serial: serialFromDeviceName(name)
|
|
75
|
+
}))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function detectWindowsSerialDevices(env) {
|
|
80
|
+
try {
|
|
81
|
+
const output = execFileSync(
|
|
82
|
+
'powershell.exe',
|
|
83
|
+
[
|
|
84
|
+
'-NoProfile',
|
|
85
|
+
'-Command',
|
|
86
|
+
'Get-CimInstance Win32_SerialPort | Select-Object DeviceID,Name,PNPDeviceID | ConvertTo-Json -Compress'
|
|
87
|
+
],
|
|
88
|
+
{ encoding: 'utf8', env, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
89
|
+
).trim()
|
|
90
|
+
if (!output) return []
|
|
91
|
+
const parsed = JSON.parse(output)
|
|
92
|
+
return (Array.isArray(parsed) ? parsed : [parsed])
|
|
93
|
+
.map((entry) => normalizeDevice({
|
|
94
|
+
path: entry.DeviceID || '',
|
|
95
|
+
label: entry.Name || '',
|
|
96
|
+
serial: serialFromDeviceName(entry.PNPDeviceID || '')
|
|
97
|
+
}))
|
|
98
|
+
.filter((device) => device.path)
|
|
99
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
100
|
+
} catch {
|
|
101
|
+
return []
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeDevice(input) {
|
|
106
|
+
if (typeof input === 'string') return normalizeDevice({ path: input })
|
|
107
|
+
const devicePath = String(input.path || '').trim()
|
|
108
|
+
const label = String(input.label || devicePath).trim()
|
|
109
|
+
const serial = String(input.serial || '').trim()
|
|
110
|
+
return {
|
|
111
|
+
path: devicePath,
|
|
112
|
+
label,
|
|
113
|
+
serial
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function serialFromLinuxById(name) {
|
|
118
|
+
const cleaned = name.replace(/-if\d+.*$/i, '')
|
|
119
|
+
const parts = cleaned.split('_').map((part) => part.trim()).filter(Boolean)
|
|
120
|
+
return parts.at(-1) || ''
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function serialFromDeviceName(name) {
|
|
124
|
+
const text = String(name || '')
|
|
125
|
+
const candidates = text.match(/[A-Za-z0-9:-]{4,}/g) || []
|
|
126
|
+
return candidates.at(-1) || ''
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function safeReaddir(dir) {
|
|
130
|
+
try {
|
|
131
|
+
return fs.readdirSync(dir)
|
|
132
|
+
} catch {
|
|
133
|
+
return []
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isDirectory(filePath) {
|
|
138
|
+
try {
|
|
139
|
+
return fs.statSync(filePath).isDirectory()
|
|
140
|
+
} catch {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|