@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/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
+ }