@11agents/cli 0.1.38 → 0.1.39
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/package.json +1 -1
- package/src/commands/runtime.js +13 -5
- package/src/mobile-device-scan.js +258 -0
- package/src/runtime-scan.js +19 -3
package/package.json
CHANGED
package/src/commands/runtime.js
CHANGED
|
@@ -149,6 +149,14 @@ function createRetryState() {
|
|
|
149
149
|
return { failures: 0 }
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
function healthFromScan(scan, extra = {}) {
|
|
153
|
+
const base = scan?.health && typeof scan.health === 'object' ? scan.health : {}
|
|
154
|
+
return {
|
|
155
|
+
...base,
|
|
156
|
+
...extra,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
async function heartbeatRegisteredRuntime(registration, flags, deps) {
|
|
153
161
|
const config = configFromFlags(flags)
|
|
154
162
|
const machineKey = registration?.machine?.machine_key || machineOverride(flags) || ''
|
|
@@ -158,9 +166,9 @@ async function heartbeatRegisteredRuntime(registration, flags, deps) {
|
|
|
158
166
|
body: {
|
|
159
167
|
machine_key: machineKey,
|
|
160
168
|
runtime_providers: (registration?.runtimes || []).map(runtime => runtime.provider).filter(Boolean),
|
|
161
|
-
health: {
|
|
169
|
+
health: healthFromScan(registration?.local_scan, {
|
|
162
170
|
heartbeat_at: new Date().toISOString(),
|
|
163
|
-
},
|
|
171
|
+
}),
|
|
164
172
|
},
|
|
165
173
|
config,
|
|
166
174
|
})
|
|
@@ -254,7 +262,7 @@ export async function registerRuntime(flags = {}, deps = {}) {
|
|
|
254
262
|
config,
|
|
255
263
|
})
|
|
256
264
|
log(JSON.stringify(result, null, 2))
|
|
257
|
-
return result
|
|
265
|
+
return { ...result, local_scan: scan }
|
|
258
266
|
}
|
|
259
267
|
|
|
260
268
|
export async function heartbeatRuntime(flags = {}, deps = {}) {
|
|
@@ -268,9 +276,9 @@ export async function heartbeatRuntime(flags = {}, deps = {}) {
|
|
|
268
276
|
body: {
|
|
269
277
|
machine_key: scan.machine.machine_key,
|
|
270
278
|
runtime_providers: scan.runtimes.map(runtime => runtime.provider),
|
|
271
|
-
health: {
|
|
279
|
+
health: healthFromScan(scan, {
|
|
272
280
|
heartbeat_at: new Date().toISOString(),
|
|
273
|
-
},
|
|
281
|
+
}),
|
|
274
282
|
},
|
|
275
283
|
config,
|
|
276
284
|
})
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { access, readFile } from 'node:fs/promises'
|
|
4
|
+
import { constants } from 'node:fs'
|
|
5
|
+
import { execFile } from 'node:child_process'
|
|
6
|
+
import { promisify } from 'node:util'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile)
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const CLI_ROOT = path.resolve(__dirname, '..')
|
|
13
|
+
const BUNDLED_DEVICES_CSV = path.join(CLI_ROOT, 'mobile-runtime', 'data-templates', 'devices.example.csv')
|
|
14
|
+
|
|
15
|
+
export const DEVICE_IDENTITY_PROPS = [
|
|
16
|
+
'ro.serialno',
|
|
17
|
+
'ro.boot.serialno',
|
|
18
|
+
'ro.vendor.boot.serialno',
|
|
19
|
+
'ro.product.serialno',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
function defaultMobileHome(env = process.env, homeDir = os.homedir()) {
|
|
23
|
+
return path.resolve(env.ELEVENAGENTS_MOBILE_HOME || path.join(homeDir, '.11agents', 'mobile'))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cleanIdentity(value = '') {
|
|
27
|
+
return String(value || '').trim().toLowerCase()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanStatus(value = '') {
|
|
31
|
+
return String(value || '').trim().toLowerCase()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isActiveRegistryRow(row) {
|
|
35
|
+
const status = cleanStatus(row.status || 'active')
|
|
36
|
+
return status !== 'inactive' && status !== 'disabled'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function deviceSortKey(device) {
|
|
40
|
+
const match = /^D(\d{2})$/i.exec(device.device_id || '')
|
|
41
|
+
if (match) return Number(match[1])
|
|
42
|
+
const fallback = String(device.device_id || device.adb_transport || device.adb_serial || device.hardware_id || '')
|
|
43
|
+
return 1000 + (fallback.charCodeAt(0) || 0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseCsvLine(line = '') {
|
|
47
|
+
const out = []
|
|
48
|
+
let value = ''
|
|
49
|
+
let quoted = false
|
|
50
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
51
|
+
const ch = line[i]
|
|
52
|
+
if (ch === '"') {
|
|
53
|
+
if (quoted && line[i + 1] === '"') {
|
|
54
|
+
value += '"'
|
|
55
|
+
i += 1
|
|
56
|
+
} else {
|
|
57
|
+
quoted = !quoted
|
|
58
|
+
}
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if (ch === ',' && !quoted) {
|
|
62
|
+
out.push(value)
|
|
63
|
+
value = ''
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
value += ch
|
|
67
|
+
}
|
|
68
|
+
out.push(value)
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseDeviceRegistryCsv(text = '') {
|
|
73
|
+
const lines = String(text || '').split(/\r?\n/).filter(line => line.trim().length)
|
|
74
|
+
if (!lines.length) return []
|
|
75
|
+
const headers = parseCsvLine(lines[0])
|
|
76
|
+
return lines.slice(1).map(line => {
|
|
77
|
+
const values = parseCsvLine(line)
|
|
78
|
+
return Object.fromEntries(headers.map((header, index) => [header, values[index] || '']))
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function canExecute(filePath, accessFn = access) {
|
|
83
|
+
try {
|
|
84
|
+
await accessFn(filePath, constants.X_OK)
|
|
85
|
+
return true
|
|
86
|
+
} catch {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function resolveAdbBin({ env = process.env, homeDir = os.homedir(), access: accessFn = access } = {}) {
|
|
92
|
+
if (env.ADB_BIN && await canExecute(env.ADB_BIN, accessFn)) return env.ADB_BIN
|
|
93
|
+
|
|
94
|
+
const pathValue = env.PATH || ''
|
|
95
|
+
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
96
|
+
const candidate = path.join(dir, process.platform === 'win32' ? 'adb.exe' : 'adb')
|
|
97
|
+
if (await canExecute(candidate, accessFn)) return candidate
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const candidates = [
|
|
101
|
+
path.join(homeDir, 'Library', 'Android', 'sdk', 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb'),
|
|
102
|
+
'/usr/local/bin/adb',
|
|
103
|
+
'/opt/homebrew/bin/adb',
|
|
104
|
+
]
|
|
105
|
+
for (const candidate of candidates) {
|
|
106
|
+
if (await canExecute(candidate, accessFn)) return candidate
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return ''
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function loadDeviceRegistry({ env, homeDir, devicesCsvPath, readFile: read = readFile } = {}) {
|
|
113
|
+
const preferred = devicesCsvPath || path.join(defaultMobileHome(env, homeDir), 'data', 'devices.csv')
|
|
114
|
+
for (const candidate of [preferred, BUNDLED_DEVICES_CSV]) {
|
|
115
|
+
try {
|
|
116
|
+
const content = await read(candidate, 'utf8')
|
|
117
|
+
return parseDeviceRegistryCsv(content).filter(isActiveRegistryRow)
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function parseAdbDevices(text = '') {
|
|
124
|
+
const devices = []
|
|
125
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
126
|
+
const trimmed = line.trim()
|
|
127
|
+
if (!trimmed || /^List of devices attached/i.test(trimmed)) continue
|
|
128
|
+
const parts = trimmed.split(/\s+/)
|
|
129
|
+
if (!parts[0]) continue
|
|
130
|
+
devices.push({
|
|
131
|
+
transport: parts[0],
|
|
132
|
+
state: parts[1] || 'unknown',
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
return devices
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function adbExec(exec, adbBin, args, timeout = 5000) {
|
|
139
|
+
return exec(adbBin, args, { timeout })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function readDeviceHardwareId({ exec, adbBin, transport }) {
|
|
143
|
+
for (const prop of DEVICE_IDENTITY_PROPS) {
|
|
144
|
+
try {
|
|
145
|
+
const { stdout } = await adbExec(exec, adbBin, ['-s', transport, 'shell', 'getprop', prop], 2500)
|
|
146
|
+
const value = String(stdout || '').trim()
|
|
147
|
+
const clean = cleanIdentity(value)
|
|
148
|
+
if (clean && !['unknown', 'null', 'none'].includes(clean)) return value
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
151
|
+
return ''
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function registryIdentityCandidates(row) {
|
|
155
|
+
return [
|
|
156
|
+
row.hardware_id,
|
|
157
|
+
row.physical_id,
|
|
158
|
+
row.adb_serial,
|
|
159
|
+
].map(cleanIdentity).filter(Boolean)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function publicDeviceRow(row, transport, hardwareId) {
|
|
163
|
+
return {
|
|
164
|
+
device_id: String(row.device_id || ''),
|
|
165
|
+
hardware_id: String(row.hardware_id || row.physical_id || hardwareId || row.adb_serial || ''),
|
|
166
|
+
adb_serial: String(row.adb_serial || ''),
|
|
167
|
+
adb_transport: transport.transport,
|
|
168
|
+
adb_state: transport.state,
|
|
169
|
+
online: transport.state === 'device',
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function unknownDeviceRow(transport, hardwareId) {
|
|
174
|
+
return {
|
|
175
|
+
device_id: '',
|
|
176
|
+
hardware_id: hardwareId || '',
|
|
177
|
+
adb_serial: '',
|
|
178
|
+
adb_transport: transport.transport,
|
|
179
|
+
adb_state: transport.state,
|
|
180
|
+
online: transport.state === 'device',
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function scanConnectedMobileDevices({
|
|
185
|
+
env = process.env,
|
|
186
|
+
homeDir = os.homedir(),
|
|
187
|
+
devicesCsvPath,
|
|
188
|
+
execFile: exec = execFileAsync,
|
|
189
|
+
access: accessFn = access,
|
|
190
|
+
readFile: read = readFile,
|
|
191
|
+
} = {}) {
|
|
192
|
+
const adbBin = await resolveAdbBin({ env, homeDir, access: accessFn })
|
|
193
|
+
if (!adbBin) {
|
|
194
|
+
return {
|
|
195
|
+
devices: [],
|
|
196
|
+
scan: { status: 'adb_missing', scanned_at: new Date().toISOString() },
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let transports = []
|
|
201
|
+
try {
|
|
202
|
+
const { stdout } = await adbExec(exec, adbBin, ['devices'], 5000)
|
|
203
|
+
transports = parseAdbDevices(stdout)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
devices: [],
|
|
207
|
+
scan: {
|
|
208
|
+
status: 'adb_failed',
|
|
209
|
+
scanned_at: new Date().toISOString(),
|
|
210
|
+
error: error instanceof Error ? error.message : String(error),
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const registry = await loadDeviceRegistry({ env, homeDir, devicesCsvPath, readFile: read })
|
|
216
|
+
const transportDetails = []
|
|
217
|
+
const transportByIdentity = new Map()
|
|
218
|
+
for (const transport of transports) {
|
|
219
|
+
const hardwareId = transport.state === 'device'
|
|
220
|
+
? await readDeviceHardwareId({ exec, adbBin, transport: transport.transport })
|
|
221
|
+
: ''
|
|
222
|
+
const detail = { ...transport, hardware_id: hardwareId }
|
|
223
|
+
transportDetails.push(detail)
|
|
224
|
+
for (const identity of [transport.transport, hardwareId].map(cleanIdentity).filter(Boolean)) {
|
|
225
|
+
if (!transportByIdentity.has(identity)) transportByIdentity.set(identity, detail)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const matchedTransports = new Set()
|
|
230
|
+
const devices = []
|
|
231
|
+
for (const row of registry) {
|
|
232
|
+
let match = null
|
|
233
|
+
for (const identity of registryIdentityCandidates(row)) {
|
|
234
|
+
match = transportByIdentity.get(identity)
|
|
235
|
+
if (match) break
|
|
236
|
+
}
|
|
237
|
+
if (!match) continue
|
|
238
|
+
matchedTransports.add(match.transport)
|
|
239
|
+
devices.push(publicDeviceRow(row, match, match.hardware_id))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const transport of transportDetails) {
|
|
243
|
+
if (!matchedTransports.has(transport.transport)) {
|
|
244
|
+
devices.push(unknownDeviceRow(transport, transport.hardware_id))
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
devices.sort((a, b) => deviceSortKey(a) - deviceSortKey(b))
|
|
249
|
+
return {
|
|
250
|
+
devices,
|
|
251
|
+
scan: {
|
|
252
|
+
status: 'ok',
|
|
253
|
+
scanned_at: new Date().toISOString(),
|
|
254
|
+
adb_path: adbBin,
|
|
255
|
+
connected_count: devices.length,
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
}
|
package/src/runtime-scan.js
CHANGED
|
@@ -4,6 +4,7 @@ import { access } from 'node:fs/promises'
|
|
|
4
4
|
import { constants } from 'node:fs'
|
|
5
5
|
import { execFile } from 'node:child_process'
|
|
6
6
|
import { promisify } from 'node:util'
|
|
7
|
+
import { scanConnectedMobileDevices } from './mobile-device-scan.js'
|
|
7
8
|
|
|
8
9
|
const execFileAsync = promisify(execFile)
|
|
9
10
|
|
|
@@ -69,6 +70,7 @@ export async function buildRuntimeScan({
|
|
|
69
70
|
cliVersion = 'dev',
|
|
70
71
|
lookupExecutable: lookup = lookupExecutable,
|
|
71
72
|
readVersion = readRuntimeVersion,
|
|
73
|
+
scanMobileDevices = scanConnectedMobileDevices,
|
|
72
74
|
} = {}) {
|
|
73
75
|
const host = hostname()
|
|
74
76
|
const runtimes = []
|
|
@@ -97,6 +99,22 @@ export async function buildRuntimeScan({
|
|
|
97
99
|
})
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
const health = {
|
|
103
|
+
scan_at: new Date().toISOString(),
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const mobile = await scanMobileDevices({ env })
|
|
107
|
+
health.mobile_devices = Array.isArray(mobile?.devices) ? mobile.devices : []
|
|
108
|
+
if (mobile?.scan) health.mobile_device_scan = mobile.scan
|
|
109
|
+
} catch (error) {
|
|
110
|
+
health.mobile_devices = []
|
|
111
|
+
health.mobile_device_scan = {
|
|
112
|
+
status: 'failed',
|
|
113
|
+
scanned_at: new Date().toISOString(),
|
|
114
|
+
error: error instanceof Error ? error.message : String(error),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
100
118
|
return {
|
|
101
119
|
machine: {
|
|
102
120
|
machine_key: machineKeyFromEnv(env, host),
|
|
@@ -107,8 +125,6 @@ export async function buildRuntimeScan({
|
|
|
107
125
|
capabilities: ['local_runtime'],
|
|
108
126
|
},
|
|
109
127
|
runtimes,
|
|
110
|
-
health
|
|
111
|
-
scan_at: new Date().toISOString(),
|
|
112
|
-
},
|
|
128
|
+
health,
|
|
113
129
|
}
|
|
114
130
|
}
|