@yoqa/runner 0.3.16 → 0.3.18
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/dist/index.js +574 -414
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16801,7 +16801,7 @@ function getCatalogDb() {
|
|
|
16801
16801
|
// package.json
|
|
16802
16802
|
var package_default = {
|
|
16803
16803
|
name: "@yoqa/runner",
|
|
16804
|
-
version: "0.3.
|
|
16804
|
+
version: "0.3.18",
|
|
16805
16805
|
description: "Yoqa local runner \u2014 Appium + catalog HTTP API (Bun). Start with yoqa serve or the yoqa-runner bin.",
|
|
16806
16806
|
license: "UNLICENSED",
|
|
16807
16807
|
type: "module",
|
|
@@ -16896,11 +16896,458 @@ function abortAllMjpegProxies() {
|
|
|
16896
16896
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
16897
16897
|
import { createServer as createServer2 } from "net";
|
|
16898
16898
|
import { homedir as homedir6 } from "os";
|
|
16899
|
-
import { join as
|
|
16899
|
+
import { join as join7 } from "path";
|
|
16900
16900
|
import { remote } from "webdriverio";
|
|
16901
|
-
|
|
16901
|
+
|
|
16902
|
+
// src/domains/devices/application.ts
|
|
16903
|
+
import { mkdtemp as mkdtemp2, readFile, rm as rm2 } from "fs/promises";
|
|
16904
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
16905
|
+
import { join as join6 } from "path";
|
|
16906
|
+
|
|
16907
|
+
// src/domains/devices/android-identity.ts
|
|
16908
|
+
function matchAndroidAppiumIdentity(deviceId, connected, knownAvds = []) {
|
|
16909
|
+
const id = deviceId.trim();
|
|
16910
|
+
if (!id)
|
|
16911
|
+
return {};
|
|
16912
|
+
const bySerial = connected.find((device) => device.serial === id);
|
|
16913
|
+
if (bySerial) {
|
|
16914
|
+
return {
|
|
16915
|
+
udid: bySerial.serial,
|
|
16916
|
+
...bySerial.avdName ? { avd: bySerial.avdName } : {}
|
|
16917
|
+
};
|
|
16918
|
+
}
|
|
16919
|
+
const byAvd = connected.find((device) => device.avdName === id);
|
|
16920
|
+
if (byAvd) {
|
|
16921
|
+
return { udid: byAvd.serial, avd: byAvd.avdName ?? id };
|
|
16922
|
+
}
|
|
16923
|
+
if (knownAvds.includes(id)) {
|
|
16924
|
+
return { avd: id };
|
|
16925
|
+
}
|
|
16926
|
+
return { udid: id };
|
|
16927
|
+
}
|
|
16928
|
+
function parseAdbEmuAvdName(stdout) {
|
|
16929
|
+
const name = stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line && line !== "OK");
|
|
16930
|
+
return name || null;
|
|
16931
|
+
}
|
|
16932
|
+
|
|
16933
|
+
// src/domains/devices/application.ts
|
|
16934
|
+
async function runCommand3(command, options) {
|
|
16935
|
+
try {
|
|
16936
|
+
const proc = Bun.spawn(command, {
|
|
16937
|
+
env: options?.env ? { ...process.env, ...options.env } : process.env,
|
|
16938
|
+
stdout: "pipe",
|
|
16939
|
+
stderr: "pipe"
|
|
16940
|
+
});
|
|
16941
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
16942
|
+
new Response(proc.stdout).text(),
|
|
16943
|
+
new Response(proc.stderr).text(),
|
|
16944
|
+
proc.exited
|
|
16945
|
+
]);
|
|
16946
|
+
return { stdout, stderr, exitCode };
|
|
16947
|
+
} catch {
|
|
16948
|
+
return { stdout: "", stderr: `failed to spawn: ${command[0]}`, exitCode: 127 };
|
|
16949
|
+
}
|
|
16950
|
+
}
|
|
16951
|
+
function androidSdkRoot() {
|
|
16952
|
+
return resolveAndroidSdkRoot();
|
|
16953
|
+
}
|
|
16954
|
+
async function resolveTool(candidates) {
|
|
16955
|
+
for (const candidate of candidates) {
|
|
16956
|
+
if (!candidate)
|
|
16957
|
+
continue;
|
|
16958
|
+
const { exitCode } = await runCommand3(["test", "-x", candidate]);
|
|
16959
|
+
if (exitCode === 0)
|
|
16960
|
+
return candidate;
|
|
16961
|
+
}
|
|
16962
|
+
return null;
|
|
16963
|
+
}
|
|
16964
|
+
async function resolveAdb() {
|
|
16965
|
+
const sdk = androidSdkRoot();
|
|
16966
|
+
return resolveTool([...sdk ? [join6(sdk, "platform-tools/adb")] : [], "adb"]);
|
|
16967
|
+
}
|
|
16968
|
+
async function resolveEmulator() {
|
|
16969
|
+
const sdk = androidSdkRoot();
|
|
16970
|
+
return resolveTool([...sdk ? [join6(sdk, "emulator/emulator")] : [], "emulator"]);
|
|
16971
|
+
}
|
|
16972
|
+
function formatIosVersion(version2) {
|
|
16973
|
+
const trimmed = version2.trim();
|
|
16974
|
+
if (!trimmed)
|
|
16975
|
+
return "iOS";
|
|
16976
|
+
if (/^iOS\b/i.test(trimmed))
|
|
16977
|
+
return trimmed;
|
|
16978
|
+
return `iOS ${trimmed}`;
|
|
16979
|
+
}
|
|
16980
|
+
function formatAndroidVersion(apiOrVersion) {
|
|
16981
|
+
const trimmed = apiOrVersion.trim();
|
|
16982
|
+
if (!trimmed)
|
|
16983
|
+
return "Android";
|
|
16984
|
+
if (/^Android\b/i.test(trimmed))
|
|
16985
|
+
return trimmed;
|
|
16986
|
+
if (/^API\b/i.test(trimmed))
|
|
16987
|
+
return trimmed;
|
|
16988
|
+
if (/^\d+(\.\d+)?$/.test(trimmed))
|
|
16989
|
+
return `API ${trimmed}`;
|
|
16990
|
+
return `Android ${trimmed}`;
|
|
16991
|
+
}
|
|
16992
|
+
function titleCaseWords(value) {
|
|
16993
|
+
return value.split(/\s+/).filter(Boolean).map((word) => {
|
|
16994
|
+
if (/^[a-z]$/i.test(word))
|
|
16995
|
+
return word.toUpperCase();
|
|
16996
|
+
if (/^\d/.test(word))
|
|
16997
|
+
return word;
|
|
16998
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
16999
|
+
}).join(" ");
|
|
17000
|
+
}
|
|
17001
|
+
function runtimeVersionFromIdentifier(runtimeId) {
|
|
17002
|
+
const match = runtimeId.match(/iOS-([\d-]+)$/i);
|
|
17003
|
+
if (!match?.[1])
|
|
17004
|
+
return "iOS";
|
|
17005
|
+
return formatIosVersion(match[1].replace(/-/g, "."));
|
|
17006
|
+
}
|
|
17007
|
+
async function listIosSimulators(includeUnavailable) {
|
|
17008
|
+
const { stdout, exitCode } = await runCommand3([
|
|
17009
|
+
"xcrun",
|
|
17010
|
+
"simctl",
|
|
17011
|
+
"list",
|
|
17012
|
+
"devices",
|
|
17013
|
+
"available",
|
|
17014
|
+
"-j"
|
|
17015
|
+
]);
|
|
17016
|
+
if (exitCode !== 0)
|
|
17017
|
+
return [];
|
|
17018
|
+
let parsed;
|
|
17019
|
+
try {
|
|
17020
|
+
parsed = JSON.parse(stdout);
|
|
17021
|
+
} catch {
|
|
17022
|
+
return [];
|
|
17023
|
+
}
|
|
17024
|
+
const devices = [];
|
|
17025
|
+
for (const [runtimeId, runtimeDevices] of Object.entries(parsed.devices ?? {})) {
|
|
17026
|
+
if (!/iOS/i.test(runtimeId))
|
|
17027
|
+
continue;
|
|
17028
|
+
const osVersion = runtimeVersionFromIdentifier(runtimeId);
|
|
17029
|
+
for (const device of runtimeDevices) {
|
|
17030
|
+
if (!device.udid || !device.name)
|
|
17031
|
+
continue;
|
|
17032
|
+
if (device.isAvailable === false)
|
|
17033
|
+
continue;
|
|
17034
|
+
const state = device.state ?? "Unknown";
|
|
17035
|
+
if (!includeUnavailable && state !== "Booted")
|
|
17036
|
+
continue;
|
|
17037
|
+
devices.push({
|
|
17038
|
+
id: device.udid,
|
|
17039
|
+
name: device.name,
|
|
17040
|
+
osVersion,
|
|
17041
|
+
platform: "ios",
|
|
17042
|
+
kind: "simulator",
|
|
17043
|
+
state,
|
|
17044
|
+
model: device.deviceTypeIdentifier?.split(".").pop()?.replace(/-/g, " ")
|
|
17045
|
+
});
|
|
17046
|
+
}
|
|
17047
|
+
}
|
|
17048
|
+
return devices.sort((a, b) => {
|
|
17049
|
+
if (a.state === "Booted" && b.state !== "Booted")
|
|
17050
|
+
return -1;
|
|
17051
|
+
if (b.state === "Booted" && a.state !== "Booted")
|
|
17052
|
+
return 1;
|
|
17053
|
+
return a.name.localeCompare(b.name);
|
|
17054
|
+
});
|
|
17055
|
+
}
|
|
17056
|
+
function isPhoneOrTablet(deviceType) {
|
|
17057
|
+
if (!deviceType)
|
|
17058
|
+
return false;
|
|
17059
|
+
return /^(iPhone|iPad)$/i.test(deviceType);
|
|
17060
|
+
}
|
|
17061
|
+
function physicalIosState(connection) {
|
|
17062
|
+
const tunnel = connection?.tunnelState?.toLowerCase() ?? "";
|
|
17063
|
+
if (tunnel.includes("connected"))
|
|
17064
|
+
return "connected";
|
|
17065
|
+
if (tunnel.includes("unavailable"))
|
|
17066
|
+
return "unavailable";
|
|
17067
|
+
if (tunnel.includes("disconnected"))
|
|
17068
|
+
return "disconnected";
|
|
17069
|
+
if (connection?.pairingState === "paired")
|
|
17070
|
+
return "paired";
|
|
17071
|
+
return tunnel || connection?.pairingState || "unknown";
|
|
17072
|
+
}
|
|
17073
|
+
async function listIosPhysicalDevices(includeUnavailable) {
|
|
17074
|
+
const dir = await mkdtemp2(join6(tmpdir2(), "yoqa-devices-"));
|
|
17075
|
+
const jsonPath = join6(dir, "devices.json");
|
|
17076
|
+
try {
|
|
17077
|
+
const { exitCode } = await runCommand3([
|
|
17078
|
+
"xcrun",
|
|
17079
|
+
"devicectl",
|
|
17080
|
+
"list",
|
|
17081
|
+
"devices",
|
|
17082
|
+
"--json-output",
|
|
17083
|
+
jsonPath
|
|
17084
|
+
]);
|
|
17085
|
+
if (exitCode !== 0)
|
|
17086
|
+
return [];
|
|
17087
|
+
let parsed;
|
|
17088
|
+
try {
|
|
17089
|
+
parsed = JSON.parse(await readFile(jsonPath, "utf8"));
|
|
17090
|
+
} catch {
|
|
17091
|
+
return [];
|
|
17092
|
+
}
|
|
17093
|
+
const devices = [];
|
|
17094
|
+
for (const item of parsed.result?.devices ?? []) {
|
|
17095
|
+
const hardware = item.hardwareProperties;
|
|
17096
|
+
const props = item.deviceProperties;
|
|
17097
|
+
if (!hardware?.udid)
|
|
17098
|
+
continue;
|
|
17099
|
+
if (hardware.reality && hardware.reality !== "physical")
|
|
17100
|
+
continue;
|
|
17101
|
+
if (!isPhoneOrTablet(hardware.deviceType))
|
|
17102
|
+
continue;
|
|
17103
|
+
if (hardware.platform && !/^iOS$/i.test(hardware.platform))
|
|
17104
|
+
continue;
|
|
17105
|
+
const state = physicalIosState(item.connectionProperties);
|
|
17106
|
+
if (!includeUnavailable && state === "unavailable")
|
|
17107
|
+
continue;
|
|
17108
|
+
const modelName = hardware.marketingName ?? hardware.productType ?? "iPhone";
|
|
17109
|
+
const owner = props?.name?.trim() || undefined;
|
|
17110
|
+
devices.push({
|
|
17111
|
+
id: hardware.udid,
|
|
17112
|
+
name: modelName,
|
|
17113
|
+
owner,
|
|
17114
|
+
osVersion: formatIosVersion(props?.osVersionNumber ?? ""),
|
|
17115
|
+
platform: "ios",
|
|
17116
|
+
kind: "physical",
|
|
17117
|
+
state,
|
|
17118
|
+
model: hardware.productType
|
|
17119
|
+
});
|
|
17120
|
+
}
|
|
17121
|
+
return devices.sort((a, b) => a.name.localeCompare(b.name));
|
|
17122
|
+
} finally {
|
|
17123
|
+
await rm2(dir, { recursive: true, force: true });
|
|
17124
|
+
}
|
|
17125
|
+
}
|
|
17126
|
+
async function listIosDevices(options = {}) {
|
|
17127
|
+
const includeUnavailable = options.includeUnavailable ?? true;
|
|
17128
|
+
const [physical, simulators] = await Promise.all([
|
|
17129
|
+
listIosPhysicalDevices(includeUnavailable),
|
|
17130
|
+
listIosSimulators(includeUnavailable)
|
|
17131
|
+
]);
|
|
17132
|
+
return [...physical, ...simulators];
|
|
17133
|
+
}
|
|
17134
|
+
function parseAdbDevices(output) {
|
|
17135
|
+
const lines = output.split(`
|
|
17136
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
17137
|
+
const results = [];
|
|
17138
|
+
for (const line of lines) {
|
|
17139
|
+
if (line.startsWith("List of devices"))
|
|
17140
|
+
continue;
|
|
17141
|
+
const parts = line.split(/\s+/);
|
|
17142
|
+
const serial = parts[0];
|
|
17143
|
+
const state = parts[1];
|
|
17144
|
+
if (!serial || !state)
|
|
17145
|
+
continue;
|
|
17146
|
+
const meta = {};
|
|
17147
|
+
for (const part of parts.slice(2)) {
|
|
17148
|
+
const eq2 = part.indexOf(":");
|
|
17149
|
+
if (eq2 <= 0)
|
|
17150
|
+
continue;
|
|
17151
|
+
meta[part.slice(0, eq2)] = part.slice(eq2 + 1);
|
|
17152
|
+
}
|
|
17153
|
+
results.push({
|
|
17154
|
+
serial,
|
|
17155
|
+
state,
|
|
17156
|
+
product: meta.product,
|
|
17157
|
+
model: meta.model,
|
|
17158
|
+
device: meta.device,
|
|
17159
|
+
transportId: meta.transport_id
|
|
17160
|
+
});
|
|
17161
|
+
}
|
|
17162
|
+
return results;
|
|
17163
|
+
}
|
|
17164
|
+
function humanizeAvdName(avdName) {
|
|
17165
|
+
return titleCaseWords(avdName.replace(/_/g, " "));
|
|
17166
|
+
}
|
|
17167
|
+
function humanizeModel(model) {
|
|
17168
|
+
if (!model)
|
|
17169
|
+
return;
|
|
17170
|
+
return titleCaseWords(model.replace(/_/g, " "));
|
|
17171
|
+
}
|
|
17172
|
+
async function findAvdConfigPath(avdName) {
|
|
17173
|
+
const home = process.env.HOME ?? "";
|
|
17174
|
+
const roots = [join6(home, ".android/avd"), join6(home, "Library/Android/sdk/.android/avd")];
|
|
17175
|
+
for (const root of roots) {
|
|
17176
|
+
const direct = join6(root, `${avdName}.avd`, "config.ini");
|
|
17177
|
+
try {
|
|
17178
|
+
await readFile(direct, "utf8");
|
|
17179
|
+
return direct;
|
|
17180
|
+
} catch {}
|
|
17181
|
+
try {
|
|
17182
|
+
const glob = new Bun.Glob("*.avd/config.ini");
|
|
17183
|
+
for await (const match of glob.scan({ cwd: root, onlyFiles: true })) {
|
|
17184
|
+
const path = join6(root, match);
|
|
17185
|
+
const text2 = await readFile(path, "utf8");
|
|
17186
|
+
const idLine = text2.split(`
|
|
17187
|
+
`).find((line) => line.startsWith("AvdId="));
|
|
17188
|
+
const id = idLine?.slice("AvdId=".length).trim();
|
|
17189
|
+
if (id === avdName)
|
|
17190
|
+
return path;
|
|
17191
|
+
}
|
|
17192
|
+
} catch {}
|
|
17193
|
+
}
|
|
17194
|
+
return null;
|
|
17195
|
+
}
|
|
17196
|
+
async function readAvdConfig(avdName) {
|
|
17197
|
+
const path = await findAvdConfigPath(avdName);
|
|
17198
|
+
if (!path)
|
|
17199
|
+
return { api: null, deviceName: null };
|
|
17200
|
+
try {
|
|
17201
|
+
const text2 = await readFile(path, "utf8");
|
|
17202
|
+
const imageLine = text2.split(`
|
|
17203
|
+
`).find((line) => line.includes("system-images") && line.includes("android-"));
|
|
17204
|
+
const apiMatch = imageLine?.match(/android-([\d.]+)/);
|
|
17205
|
+
const deviceLine = text2.split(`
|
|
17206
|
+
`).find((entry) => entry.startsWith("hw.device.name="));
|
|
17207
|
+
const deviceRaw = deviceLine?.slice("hw.device.name=".length).trim();
|
|
17208
|
+
return {
|
|
17209
|
+
api: apiMatch?.[1] ?? null,
|
|
17210
|
+
deviceName: humanizeModel(deviceRaw) ?? null
|
|
17211
|
+
};
|
|
17212
|
+
} catch {
|
|
17213
|
+
return { api: null, deviceName: null };
|
|
17214
|
+
}
|
|
17215
|
+
}
|
|
17216
|
+
async function adbAvdName(adb, serial) {
|
|
17217
|
+
const fromProp = await adbGetProp(adb, serial, "ro.boot.qemu.avd_name") ?? await adbGetProp(adb, serial, "ro.kernel.qemu.avd_name");
|
|
17218
|
+
if (fromProp)
|
|
17219
|
+
return fromProp;
|
|
17220
|
+
const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "emu", "avd", "name"]);
|
|
17221
|
+
if (exitCode !== 0)
|
|
17222
|
+
return null;
|
|
17223
|
+
return parseAdbEmuAvdName(stdout);
|
|
17224
|
+
}
|
|
17225
|
+
async function adbGetProp(adb, serial, prop) {
|
|
17226
|
+
const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "shell", "getprop", prop]);
|
|
17227
|
+
if (exitCode !== 0)
|
|
17228
|
+
return null;
|
|
17229
|
+
const value = stdout.trim();
|
|
17230
|
+
return value || null;
|
|
17231
|
+
}
|
|
17232
|
+
async function listAndroidAdbDevices(adb) {
|
|
17233
|
+
const { stdout, exitCode } = await runCommand3([adb, "devices", "-l"]);
|
|
17234
|
+
if (exitCode !== 0) {
|
|
17235
|
+
return { physical: [], runningEmulators: [], runningAvdNames: new Set };
|
|
17236
|
+
}
|
|
17237
|
+
const entries = parseAdbDevices(stdout);
|
|
17238
|
+
const physical = [];
|
|
17239
|
+
const runningEmulators = [];
|
|
17240
|
+
const runningAvdNames = new Set;
|
|
17241
|
+
for (const entry of entries) {
|
|
17242
|
+
const isEmulator = entry.serial.startsWith("emulator-") || entry.product === "sdk_gphone";
|
|
17243
|
+
if (isEmulator) {
|
|
17244
|
+
const avdName = await adbAvdName(adb, entry.serial);
|
|
17245
|
+
if (avdName)
|
|
17246
|
+
runningAvdNames.add(avdName);
|
|
17247
|
+
const avdConfig = avdName ? await readAvdConfig(avdName) : null;
|
|
17248
|
+
const release2 = await adbGetProp(adb, entry.serial, "ro.build.version.release") ?? avdConfig?.api ?? null;
|
|
17249
|
+
const model2 = humanizeModel(entry.model) ?? avdConfig?.deviceName ?? (avdName ? humanizeAvdName(avdName) : entry.serial);
|
|
17250
|
+
runningEmulators.push({
|
|
17251
|
+
id: avdName ?? entry.serial,
|
|
17252
|
+
name: model2,
|
|
17253
|
+
osVersion: formatAndroidVersion(release2 ?? ""),
|
|
17254
|
+
platform: "android",
|
|
17255
|
+
kind: "emulator",
|
|
17256
|
+
state: entry.state === "device" ? "online" : entry.state,
|
|
17257
|
+
model: avdName ?? entry.model
|
|
17258
|
+
});
|
|
17259
|
+
continue;
|
|
17260
|
+
}
|
|
17261
|
+
const release = await adbGetProp(adb, entry.serial, "ro.build.version.release");
|
|
17262
|
+
const model = humanizeModel(entry.model) ?? await adbGetProp(adb, entry.serial, "ro.product.model") ?? entry.serial;
|
|
17263
|
+
physical.push({
|
|
17264
|
+
id: entry.serial,
|
|
17265
|
+
name: model,
|
|
17266
|
+
osVersion: formatAndroidVersion(release ?? ""),
|
|
17267
|
+
platform: "android",
|
|
17268
|
+
kind: "physical",
|
|
17269
|
+
state: entry.state,
|
|
17270
|
+
model: entry.model
|
|
17271
|
+
});
|
|
17272
|
+
}
|
|
17273
|
+
return { physical, runningEmulators, runningAvdNames };
|
|
17274
|
+
}
|
|
17275
|
+
async function listAndroidAvds(emulatorBin, runningAvdNames, includeUnavailable) {
|
|
17276
|
+
const { stdout, exitCode } = await runCommand3([emulatorBin, "-list-avds"]);
|
|
17277
|
+
if (exitCode !== 0)
|
|
17278
|
+
return [];
|
|
17279
|
+
const names = stdout.split(`
|
|
17280
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
17281
|
+
const devices = [];
|
|
17282
|
+
for (const avdName of names) {
|
|
17283
|
+
const isRunning = runningAvdNames.has(avdName);
|
|
17284
|
+
if (!includeUnavailable && !isRunning)
|
|
17285
|
+
continue;
|
|
17286
|
+
if (isRunning)
|
|
17287
|
+
continue;
|
|
17288
|
+
const { api, deviceName } = await readAvdConfig(avdName);
|
|
17289
|
+
devices.push({
|
|
17290
|
+
id: avdName,
|
|
17291
|
+
name: deviceName ?? humanizeAvdName(avdName),
|
|
17292
|
+
osVersion: formatAndroidVersion(api ?? ""),
|
|
17293
|
+
platform: "android",
|
|
17294
|
+
kind: "emulator",
|
|
17295
|
+
state: "offline",
|
|
17296
|
+
model: avdName
|
|
17297
|
+
});
|
|
17298
|
+
}
|
|
17299
|
+
return devices;
|
|
17300
|
+
}
|
|
17301
|
+
async function resolveAndroidAppiumIdentity(deviceId) {
|
|
17302
|
+
const [adb, emulatorBin] = await Promise.all([resolveAdb(), resolveEmulator()]);
|
|
17303
|
+
const connected = [];
|
|
17304
|
+
if (adb) {
|
|
17305
|
+
const { stdout, exitCode } = await runCommand3([adb, "devices", "-l"]);
|
|
17306
|
+
if (exitCode === 0) {
|
|
17307
|
+
for (const entry of parseAdbDevices(stdout)) {
|
|
17308
|
+
const isEmulator = entry.serial.startsWith("emulator-") || entry.product === "sdk_gphone";
|
|
17309
|
+
connected.push({
|
|
17310
|
+
serial: entry.serial,
|
|
17311
|
+
avdName: isEmulator ? await adbAvdName(adb, entry.serial) : null
|
|
17312
|
+
});
|
|
17313
|
+
}
|
|
17314
|
+
}
|
|
17315
|
+
}
|
|
17316
|
+
const knownAvds = [];
|
|
17317
|
+
if (emulatorBin) {
|
|
17318
|
+
const { stdout, exitCode } = await runCommand3([emulatorBin, "-list-avds"]);
|
|
17319
|
+
if (exitCode === 0) {
|
|
17320
|
+
knownAvds.push(...stdout.split(`
|
|
17321
|
+
`).map((line) => line.trim()).filter(Boolean));
|
|
17322
|
+
}
|
|
17323
|
+
}
|
|
17324
|
+
return matchAndroidAppiumIdentity(deviceId, connected, knownAvds);
|
|
17325
|
+
}
|
|
17326
|
+
async function listAndroidDevices(options = {}) {
|
|
17327
|
+
const includeUnavailable = options.includeUnavailable ?? true;
|
|
17328
|
+
const [adb, emulatorBin] = await Promise.all([resolveAdb(), resolveEmulator()]);
|
|
17329
|
+
const adbResult = adb ? await listAndroidAdbDevices(adb) : { physical: [], runningEmulators: [], runningAvdNames: new Set };
|
|
17330
|
+
const offlineAvds = emulatorBin != null ? await listAndroidAvds(emulatorBin, adbResult.runningAvdNames, includeUnavailable) : [];
|
|
17331
|
+
const physical = includeUnavailable ? adbResult.physical : adbResult.physical.filter((device) => device.state === "device");
|
|
17332
|
+
const emulators = [...adbResult.runningEmulators, ...offlineAvds].sort((a, b) => {
|
|
17333
|
+
const aOnline = a.state === "online" ? 0 : 1;
|
|
17334
|
+
const bOnline = b.state === "online" ? 0 : 1;
|
|
17335
|
+
if (aOnline !== bOnline)
|
|
17336
|
+
return aOnline - bOnline;
|
|
17337
|
+
return a.name.localeCompare(b.name);
|
|
17338
|
+
});
|
|
17339
|
+
return [...physical, ...emulators];
|
|
17340
|
+
}
|
|
17341
|
+
async function listDevices(platform, options = {}) {
|
|
17342
|
+
if (platform === "ios")
|
|
17343
|
+
return listIosDevices(options);
|
|
17344
|
+
return listAndroidDevices(options);
|
|
17345
|
+
}
|
|
17346
|
+
|
|
17347
|
+
// src/domains/devices/session.ts
|
|
17348
|
+
var YOQA_ROOT4 = join7(homedir6(), ".yoqa");
|
|
16902
17349
|
var DEFAULT_MJPEG_PORT = Number(process.env.YOQA_MJPEG_PORT ?? "9100");
|
|
16903
|
-
var SCREENSHOT_DIR =
|
|
17350
|
+
var SCREENSHOT_DIR = join7(YOQA_ROOT4, "runs", "screenshots");
|
|
16904
17351
|
var MJPEG_SETTINGS_BASE = {
|
|
16905
17352
|
mjpegServerScreenshotQuality: 35,
|
|
16906
17353
|
mjpegScalingFactor: 50
|
|
@@ -17020,8 +17467,11 @@ function mergeCapabilities(appCaps, caseCaps) {
|
|
|
17020
17467
|
function looksLikePhysicalIosUdid(udid) {
|
|
17021
17468
|
return /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{16}$/.test(udid.trim());
|
|
17022
17469
|
}
|
|
17470
|
+
var PHYSICAL_IOS_SESSION_TIMEOUT_MS = 60000;
|
|
17471
|
+
var SIMULATOR_WDA_SESSION_TIMEOUT_MS = 600000;
|
|
17472
|
+
var SIMULATOR_WDA_DERIVED_DATA = join7(YOQA_ROOT4, "wda-sim");
|
|
17023
17473
|
function iosSessionCreateTimeoutMs(deviceId) {
|
|
17024
|
-
return looksLikePhysicalIosUdid(deviceId) ?
|
|
17474
|
+
return looksLikePhysicalIosUdid(deviceId) ? PHYSICAL_IOS_SESSION_TIMEOUT_MS : SIMULATOR_WDA_SESSION_TIMEOUT_MS;
|
|
17025
17475
|
}
|
|
17026
17476
|
async function buildW3cCapabilities(options, mjpegPort) {
|
|
17027
17477
|
const merged = mergeCapabilities(options.appCaps, options.caseCaps);
|
|
@@ -17030,18 +17480,17 @@ async function buildW3cCapabilities(options, mjpegPort) {
|
|
|
17030
17480
|
const caps = {
|
|
17031
17481
|
platformName,
|
|
17032
17482
|
"appium:automationName": automationName,
|
|
17033
|
-
"appium:udid": options.deviceId,
|
|
17034
17483
|
"appium:newCommandTimeout": 3600,
|
|
17035
17484
|
"appium:mjpegServerPort": mjpegPort,
|
|
17036
17485
|
...Object.fromEntries(Object.entries(merged).map(([key, value]) => key.includes(":") ? [key, value] : [`appium:${key}`, value]))
|
|
17037
17486
|
};
|
|
17038
|
-
if (options.platform === "ios" && options.bundleId && !caps["appium:bundleId"]) {
|
|
17039
|
-
caps["appium:bundleId"] = options.bundleId;
|
|
17040
|
-
}
|
|
17041
|
-
if (options.platform === "android" && options.appPackage && !caps["appium:appPackage"]) {
|
|
17042
|
-
caps["appium:appPackage"] = options.appPackage;
|
|
17043
|
-
}
|
|
17044
17487
|
if (options.platform === "ios") {
|
|
17488
|
+
if (caps["appium:udid"] === undefined) {
|
|
17489
|
+
caps["appium:udid"] = options.deviceId;
|
|
17490
|
+
}
|
|
17491
|
+
if (options.bundleId && !caps["appium:bundleId"]) {
|
|
17492
|
+
caps["appium:bundleId"] = options.bundleId;
|
|
17493
|
+
}
|
|
17045
17494
|
const physical = looksLikePhysicalIosUdid(options.deviceId);
|
|
17046
17495
|
if (physical) {
|
|
17047
17496
|
const prep = await loadDevicePrep(options.deviceId);
|
|
@@ -17071,6 +17520,23 @@ async function buildW3cCapabilities(options, mjpegPort) {
|
|
|
17071
17520
|
if (caps["appium:waitForIdleTimeout"] === undefined) {
|
|
17072
17521
|
caps["appium:waitForIdleTimeout"] = 0;
|
|
17073
17522
|
}
|
|
17523
|
+
if (!physical && caps["appium:derivedDataPath"] === undefined) {
|
|
17524
|
+
await mkdir3(SIMULATOR_WDA_DERIVED_DATA, { recursive: true });
|
|
17525
|
+
caps["appium:derivedDataPath"] = SIMULATOR_WDA_DERIVED_DATA;
|
|
17526
|
+
}
|
|
17527
|
+
}
|
|
17528
|
+
if (options.platform === "android") {
|
|
17529
|
+
const requested = String(caps["appium:udid"] ?? options.deviceId);
|
|
17530
|
+
const identity = await resolveAndroidAppiumIdentity(requested);
|
|
17531
|
+
if (identity.udid) {
|
|
17532
|
+
caps["appium:udid"] = identity.udid;
|
|
17533
|
+
}
|
|
17534
|
+
if (identity.avd && caps["appium:avd"] === undefined) {
|
|
17535
|
+
caps["appium:avd"] = identity.avd;
|
|
17536
|
+
}
|
|
17537
|
+
if (options.appPackage && !caps["appium:appPackage"]) {
|
|
17538
|
+
caps["appium:appPackage"] = options.appPackage;
|
|
17539
|
+
}
|
|
17074
17540
|
}
|
|
17075
17541
|
return caps;
|
|
17076
17542
|
}
|
|
@@ -17362,7 +17828,7 @@ async function createDeviceSession(options) {
|
|
|
17362
17828
|
const screenshot = async () => {
|
|
17363
17829
|
await mkdir3(SCREENSHOT_DIR, { recursive: true });
|
|
17364
17830
|
const frame = await captureFrame();
|
|
17365
|
-
const path =
|
|
17831
|
+
const path = join7(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
|
|
17366
17832
|
await Bun.write(path, Uint8Array.from(Buffer.from(frame.base64, "base64")));
|
|
17367
17833
|
return { path, base64: frame.base64 };
|
|
17368
17834
|
};
|
|
@@ -23551,13 +24017,24 @@ var caseFlowStepSchema = exports_external.object({
|
|
|
23551
24017
|
flowId: exports_external.string().min(1).nullable().optional()
|
|
23552
24018
|
});
|
|
23553
24019
|
var caseRunStatusSchema = exports_external.union([exports_external.literal("passed"), exports_external.literal("errored")]);
|
|
23554
|
-
var
|
|
23555
|
-
exports_external.
|
|
23556
|
-
|
|
23557
|
-
|
|
23558
|
-
|
|
23559
|
-
|
|
23560
|
-
|
|
24020
|
+
var caseScriptTapSchema = exports_external.object({
|
|
24021
|
+
type: exports_external.literal("tap"),
|
|
24022
|
+
x: exports_external.number().min(0).max(1000).optional(),
|
|
24023
|
+
y: exports_external.number().min(0).max(1000).optional(),
|
|
24024
|
+
label: exports_external.string().min(1).optional(),
|
|
24025
|
+
id: exports_external.string().min(1).optional(),
|
|
24026
|
+
reason: exports_external.string().optional()
|
|
24027
|
+
}).superRefine((value, ctx) => {
|
|
24028
|
+
const hasCoords = value.x != null && value.y != null;
|
|
24029
|
+
if (!hasCoords && !value.label && !value.id) {
|
|
24030
|
+
ctx.addIssue({
|
|
24031
|
+
code: exports_external.ZodIssueCode.custom,
|
|
24032
|
+
message: "tap requires x/y or label or id"
|
|
24033
|
+
});
|
|
24034
|
+
}
|
|
24035
|
+
});
|
|
24036
|
+
var caseScriptActionSchema = exports_external.union([
|
|
24037
|
+
caseScriptTapSchema,
|
|
23561
24038
|
exports_external.object({
|
|
23562
24039
|
type: exports_external.literal("type"),
|
|
23563
24040
|
text: exports_external.string(),
|
|
@@ -23567,6 +24044,13 @@ var caseScriptActionSchema = exports_external.discriminatedUnion("type", [
|
|
|
23567
24044
|
type: exports_external.literal("wait"),
|
|
23568
24045
|
ms: exports_external.number().min(0).max(1e4),
|
|
23569
24046
|
reason: exports_external.string().optional()
|
|
24047
|
+
}),
|
|
24048
|
+
exports_external.object({
|
|
24049
|
+
type: exports_external.literal("assert"),
|
|
24050
|
+
assertion: exports_external.union([exports_external.literal("visible"), exports_external.literal("not-visible")]).default("visible"),
|
|
24051
|
+
text: exports_external.string().min(1),
|
|
24052
|
+
timeoutMs: exports_external.number().min(0).max(120000).optional(),
|
|
24053
|
+
reason: exports_external.string().optional()
|
|
23570
24054
|
})
|
|
23571
24055
|
]);
|
|
23572
24056
|
var caseScriptSchema = exports_external.object({
|
|
@@ -23975,6 +24459,9 @@ var createBuildRequestSchema = exports_external.object({
|
|
|
23975
24459
|
var listBuildsResponseSchema = exports_external.object({
|
|
23976
24460
|
builds: exports_external.array(buildSchema)
|
|
23977
24461
|
});
|
|
24462
|
+
|
|
24463
|
+
// ../../packages/runner-client/src/wait-for-run.ts
|
|
24464
|
+
var DEFAULT_WAIT_FOR_RUN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
23978
24465
|
// ../../packages/runner-client/src/shell-script.ts
|
|
23979
24466
|
var DEFAULT_SHELL_SCRIPT_HEADER = [
|
|
23980
24467
|
"#!/usr/bin/env bash",
|
|
@@ -23984,6 +24471,16 @@ var DEFAULT_SHELL_SCRIPT_HEADER = [
|
|
|
23984
24471
|
""
|
|
23985
24472
|
].join(`
|
|
23986
24473
|
`);
|
|
24474
|
+
function screenHasText(elements, text2) {
|
|
24475
|
+
const needle = text2.trim().toLowerCase();
|
|
24476
|
+
if (!needle)
|
|
24477
|
+
return false;
|
|
24478
|
+
return (elements ?? []).some((el) => {
|
|
24479
|
+
const label = el.label?.toLowerCase() ?? "";
|
|
24480
|
+
const type = el.type?.toLowerCase() ?? "";
|
|
24481
|
+
return label.includes(needle) || type.includes(needle);
|
|
24482
|
+
});
|
|
24483
|
+
}
|
|
23987
24484
|
function findElementByLabel(elements, label) {
|
|
23988
24485
|
const needle = label.trim().toLowerCase();
|
|
23989
24486
|
if (!needle)
|
|
@@ -24155,8 +24652,13 @@ async function installBuildOnDevice(options) {
|
|
|
24155
24652
|
}
|
|
24156
24653
|
throw new Error(`Cannot install iOS build at ${build.path}. Use a .app on simulator or install .ipa via ideviceinstaller.`);
|
|
24157
24654
|
}
|
|
24655
|
+
const identity = await resolveAndroidAppiumIdentity(deviceId);
|
|
24656
|
+
const serial = identity.udid;
|
|
24657
|
+
if (!serial) {
|
|
24658
|
+
throw new Error(`Android emulator ${deviceId} is not running. Boot it, then retry the install.`);
|
|
24659
|
+
}
|
|
24158
24660
|
const adb = Bun.which("adb") ?? "adb";
|
|
24159
|
-
const proc = Bun.spawn([adb, "-s",
|
|
24661
|
+
const proc = Bun.spawn([adb, "-s", serial, "install", "-r", build.path], {
|
|
24160
24662
|
stdout: "pipe",
|
|
24161
24663
|
stderr: "pipe"
|
|
24162
24664
|
});
|
|
@@ -24979,397 +25481,6 @@ function createCatalogRoutes() {
|
|
|
24979
25481
|
return app;
|
|
24980
25482
|
}
|
|
24981
25483
|
|
|
24982
|
-
// src/domains/devices/application.ts
|
|
24983
|
-
import { mkdtemp as mkdtemp2, readFile, rm as rm2 } from "fs/promises";
|
|
24984
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
24985
|
-
import { join as join7 } from "path";
|
|
24986
|
-
async function runCommand3(command, options) {
|
|
24987
|
-
try {
|
|
24988
|
-
const proc = Bun.spawn(command, {
|
|
24989
|
-
env: options?.env ? { ...process.env, ...options.env } : process.env,
|
|
24990
|
-
stdout: "pipe",
|
|
24991
|
-
stderr: "pipe"
|
|
24992
|
-
});
|
|
24993
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
24994
|
-
new Response(proc.stdout).text(),
|
|
24995
|
-
new Response(proc.stderr).text(),
|
|
24996
|
-
proc.exited
|
|
24997
|
-
]);
|
|
24998
|
-
return { stdout, stderr, exitCode };
|
|
24999
|
-
} catch {
|
|
25000
|
-
return { stdout: "", stderr: `failed to spawn: ${command[0]}`, exitCode: 127 };
|
|
25001
|
-
}
|
|
25002
|
-
}
|
|
25003
|
-
function androidSdkRoot() {
|
|
25004
|
-
return resolveAndroidSdkRoot();
|
|
25005
|
-
}
|
|
25006
|
-
async function resolveTool(candidates) {
|
|
25007
|
-
for (const candidate of candidates) {
|
|
25008
|
-
if (!candidate)
|
|
25009
|
-
continue;
|
|
25010
|
-
const { exitCode } = await runCommand3(["test", "-x", candidate]);
|
|
25011
|
-
if (exitCode === 0)
|
|
25012
|
-
return candidate;
|
|
25013
|
-
}
|
|
25014
|
-
return null;
|
|
25015
|
-
}
|
|
25016
|
-
async function resolveAdb() {
|
|
25017
|
-
const sdk = androidSdkRoot();
|
|
25018
|
-
return resolveTool([...sdk ? [join7(sdk, "platform-tools/adb")] : [], "adb"]);
|
|
25019
|
-
}
|
|
25020
|
-
async function resolveEmulator() {
|
|
25021
|
-
const sdk = androidSdkRoot();
|
|
25022
|
-
return resolveTool([...sdk ? [join7(sdk, "emulator/emulator")] : [], "emulator"]);
|
|
25023
|
-
}
|
|
25024
|
-
function formatIosVersion(version2) {
|
|
25025
|
-
const trimmed = version2.trim();
|
|
25026
|
-
if (!trimmed)
|
|
25027
|
-
return "iOS";
|
|
25028
|
-
if (/^iOS\b/i.test(trimmed))
|
|
25029
|
-
return trimmed;
|
|
25030
|
-
return `iOS ${trimmed}`;
|
|
25031
|
-
}
|
|
25032
|
-
function formatAndroidVersion(apiOrVersion) {
|
|
25033
|
-
const trimmed = apiOrVersion.trim();
|
|
25034
|
-
if (!trimmed)
|
|
25035
|
-
return "Android";
|
|
25036
|
-
if (/^Android\b/i.test(trimmed))
|
|
25037
|
-
return trimmed;
|
|
25038
|
-
if (/^API\b/i.test(trimmed))
|
|
25039
|
-
return trimmed;
|
|
25040
|
-
if (/^\d+(\.\d+)?$/.test(trimmed))
|
|
25041
|
-
return `API ${trimmed}`;
|
|
25042
|
-
return `Android ${trimmed}`;
|
|
25043
|
-
}
|
|
25044
|
-
function titleCaseWords(value) {
|
|
25045
|
-
return value.split(/\s+/).filter(Boolean).map((word) => {
|
|
25046
|
-
if (/^[a-z]$/i.test(word))
|
|
25047
|
-
return word.toUpperCase();
|
|
25048
|
-
if (/^\d/.test(word))
|
|
25049
|
-
return word;
|
|
25050
|
-
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
25051
|
-
}).join(" ");
|
|
25052
|
-
}
|
|
25053
|
-
function runtimeVersionFromIdentifier(runtimeId) {
|
|
25054
|
-
const match2 = runtimeId.match(/iOS-([\d-]+)$/i);
|
|
25055
|
-
if (!match2?.[1])
|
|
25056
|
-
return "iOS";
|
|
25057
|
-
return formatIosVersion(match2[1].replace(/-/g, "."));
|
|
25058
|
-
}
|
|
25059
|
-
async function listIosSimulators(includeUnavailable) {
|
|
25060
|
-
const { stdout, exitCode } = await runCommand3([
|
|
25061
|
-
"xcrun",
|
|
25062
|
-
"simctl",
|
|
25063
|
-
"list",
|
|
25064
|
-
"devices",
|
|
25065
|
-
"available",
|
|
25066
|
-
"-j"
|
|
25067
|
-
]);
|
|
25068
|
-
if (exitCode !== 0)
|
|
25069
|
-
return [];
|
|
25070
|
-
let parsed;
|
|
25071
|
-
try {
|
|
25072
|
-
parsed = JSON.parse(stdout);
|
|
25073
|
-
} catch {
|
|
25074
|
-
return [];
|
|
25075
|
-
}
|
|
25076
|
-
const devices = [];
|
|
25077
|
-
for (const [runtimeId, runtimeDevices] of Object.entries(parsed.devices ?? {})) {
|
|
25078
|
-
if (!/iOS/i.test(runtimeId))
|
|
25079
|
-
continue;
|
|
25080
|
-
const osVersion = runtimeVersionFromIdentifier(runtimeId);
|
|
25081
|
-
for (const device of runtimeDevices) {
|
|
25082
|
-
if (!device.udid || !device.name)
|
|
25083
|
-
continue;
|
|
25084
|
-
if (device.isAvailable === false)
|
|
25085
|
-
continue;
|
|
25086
|
-
const state = device.state ?? "Unknown";
|
|
25087
|
-
if (!includeUnavailable && state !== "Booted")
|
|
25088
|
-
continue;
|
|
25089
|
-
devices.push({
|
|
25090
|
-
id: device.udid,
|
|
25091
|
-
name: device.name,
|
|
25092
|
-
osVersion,
|
|
25093
|
-
platform: "ios",
|
|
25094
|
-
kind: "simulator",
|
|
25095
|
-
state,
|
|
25096
|
-
model: device.deviceTypeIdentifier?.split(".").pop()?.replace(/-/g, " ")
|
|
25097
|
-
});
|
|
25098
|
-
}
|
|
25099
|
-
}
|
|
25100
|
-
return devices.sort((a, b) => {
|
|
25101
|
-
if (a.state === "Booted" && b.state !== "Booted")
|
|
25102
|
-
return -1;
|
|
25103
|
-
if (b.state === "Booted" && a.state !== "Booted")
|
|
25104
|
-
return 1;
|
|
25105
|
-
return a.name.localeCompare(b.name);
|
|
25106
|
-
});
|
|
25107
|
-
}
|
|
25108
|
-
function isPhoneOrTablet(deviceType) {
|
|
25109
|
-
if (!deviceType)
|
|
25110
|
-
return false;
|
|
25111
|
-
return /^(iPhone|iPad)$/i.test(deviceType);
|
|
25112
|
-
}
|
|
25113
|
-
function physicalIosState(connection) {
|
|
25114
|
-
const tunnel = connection?.tunnelState?.toLowerCase() ?? "";
|
|
25115
|
-
if (tunnel.includes("connected"))
|
|
25116
|
-
return "connected";
|
|
25117
|
-
if (tunnel.includes("unavailable"))
|
|
25118
|
-
return "unavailable";
|
|
25119
|
-
if (tunnel.includes("disconnected"))
|
|
25120
|
-
return "disconnected";
|
|
25121
|
-
if (connection?.pairingState === "paired")
|
|
25122
|
-
return "paired";
|
|
25123
|
-
return tunnel || connection?.pairingState || "unknown";
|
|
25124
|
-
}
|
|
25125
|
-
async function listIosPhysicalDevices(includeUnavailable) {
|
|
25126
|
-
const dir = await mkdtemp2(join7(tmpdir2(), "yoqa-devices-"));
|
|
25127
|
-
const jsonPath = join7(dir, "devices.json");
|
|
25128
|
-
try {
|
|
25129
|
-
const { exitCode } = await runCommand3([
|
|
25130
|
-
"xcrun",
|
|
25131
|
-
"devicectl",
|
|
25132
|
-
"list",
|
|
25133
|
-
"devices",
|
|
25134
|
-
"--json-output",
|
|
25135
|
-
jsonPath
|
|
25136
|
-
]);
|
|
25137
|
-
if (exitCode !== 0)
|
|
25138
|
-
return [];
|
|
25139
|
-
let parsed;
|
|
25140
|
-
try {
|
|
25141
|
-
parsed = JSON.parse(await readFile(jsonPath, "utf8"));
|
|
25142
|
-
} catch {
|
|
25143
|
-
return [];
|
|
25144
|
-
}
|
|
25145
|
-
const devices = [];
|
|
25146
|
-
for (const item of parsed.result?.devices ?? []) {
|
|
25147
|
-
const hardware = item.hardwareProperties;
|
|
25148
|
-
const props = item.deviceProperties;
|
|
25149
|
-
if (!hardware?.udid)
|
|
25150
|
-
continue;
|
|
25151
|
-
if (hardware.reality && hardware.reality !== "physical")
|
|
25152
|
-
continue;
|
|
25153
|
-
if (!isPhoneOrTablet(hardware.deviceType))
|
|
25154
|
-
continue;
|
|
25155
|
-
if (hardware.platform && !/^iOS$/i.test(hardware.platform))
|
|
25156
|
-
continue;
|
|
25157
|
-
const state = physicalIosState(item.connectionProperties);
|
|
25158
|
-
if (!includeUnavailable && state === "unavailable")
|
|
25159
|
-
continue;
|
|
25160
|
-
const modelName = hardware.marketingName ?? hardware.productType ?? "iPhone";
|
|
25161
|
-
const owner = props?.name?.trim() || undefined;
|
|
25162
|
-
devices.push({
|
|
25163
|
-
id: hardware.udid,
|
|
25164
|
-
name: modelName,
|
|
25165
|
-
owner,
|
|
25166
|
-
osVersion: formatIosVersion(props?.osVersionNumber ?? ""),
|
|
25167
|
-
platform: "ios",
|
|
25168
|
-
kind: "physical",
|
|
25169
|
-
state,
|
|
25170
|
-
model: hardware.productType
|
|
25171
|
-
});
|
|
25172
|
-
}
|
|
25173
|
-
return devices.sort((a, b) => a.name.localeCompare(b.name));
|
|
25174
|
-
} finally {
|
|
25175
|
-
await rm2(dir, { recursive: true, force: true });
|
|
25176
|
-
}
|
|
25177
|
-
}
|
|
25178
|
-
async function listIosDevices(options = {}) {
|
|
25179
|
-
const includeUnavailable = options.includeUnavailable ?? true;
|
|
25180
|
-
const [physical, simulators] = await Promise.all([
|
|
25181
|
-
listIosPhysicalDevices(includeUnavailable),
|
|
25182
|
-
listIosSimulators(includeUnavailable)
|
|
25183
|
-
]);
|
|
25184
|
-
return [...physical, ...simulators];
|
|
25185
|
-
}
|
|
25186
|
-
function parseAdbDevices(output) {
|
|
25187
|
-
const lines = output.split(`
|
|
25188
|
-
`).map((line) => line.trim()).filter(Boolean);
|
|
25189
|
-
const results = [];
|
|
25190
|
-
for (const line of lines) {
|
|
25191
|
-
if (line.startsWith("List of devices"))
|
|
25192
|
-
continue;
|
|
25193
|
-
const parts = line.split(/\s+/);
|
|
25194
|
-
const serial = parts[0];
|
|
25195
|
-
const state = parts[1];
|
|
25196
|
-
if (!serial || !state)
|
|
25197
|
-
continue;
|
|
25198
|
-
const meta = {};
|
|
25199
|
-
for (const part of parts.slice(2)) {
|
|
25200
|
-
const eq2 = part.indexOf(":");
|
|
25201
|
-
if (eq2 <= 0)
|
|
25202
|
-
continue;
|
|
25203
|
-
meta[part.slice(0, eq2)] = part.slice(eq2 + 1);
|
|
25204
|
-
}
|
|
25205
|
-
results.push({
|
|
25206
|
-
serial,
|
|
25207
|
-
state,
|
|
25208
|
-
product: meta.product,
|
|
25209
|
-
model: meta.model,
|
|
25210
|
-
device: meta.device,
|
|
25211
|
-
transportId: meta.transport_id
|
|
25212
|
-
});
|
|
25213
|
-
}
|
|
25214
|
-
return results;
|
|
25215
|
-
}
|
|
25216
|
-
function humanizeAvdName(avdName) {
|
|
25217
|
-
return titleCaseWords(avdName.replace(/_/g, " "));
|
|
25218
|
-
}
|
|
25219
|
-
function humanizeModel(model) {
|
|
25220
|
-
if (!model)
|
|
25221
|
-
return;
|
|
25222
|
-
return titleCaseWords(model.replace(/_/g, " "));
|
|
25223
|
-
}
|
|
25224
|
-
async function findAvdConfigPath(avdName) {
|
|
25225
|
-
const home = process.env.HOME ?? "";
|
|
25226
|
-
const roots = [join7(home, ".android/avd"), join7(home, "Library/Android/sdk/.android/avd")];
|
|
25227
|
-
for (const root of roots) {
|
|
25228
|
-
const direct = join7(root, `${avdName}.avd`, "config.ini");
|
|
25229
|
-
try {
|
|
25230
|
-
await readFile(direct, "utf8");
|
|
25231
|
-
return direct;
|
|
25232
|
-
} catch {}
|
|
25233
|
-
try {
|
|
25234
|
-
const glob = new Bun.Glob("*.avd/config.ini");
|
|
25235
|
-
for await (const match2 of glob.scan({ cwd: root, onlyFiles: true })) {
|
|
25236
|
-
const path = join7(root, match2);
|
|
25237
|
-
const text2 = await readFile(path, "utf8");
|
|
25238
|
-
const idLine = text2.split(`
|
|
25239
|
-
`).find((line) => line.startsWith("AvdId="));
|
|
25240
|
-
const id = idLine?.slice("AvdId=".length).trim();
|
|
25241
|
-
if (id === avdName)
|
|
25242
|
-
return path;
|
|
25243
|
-
}
|
|
25244
|
-
} catch {}
|
|
25245
|
-
}
|
|
25246
|
-
return null;
|
|
25247
|
-
}
|
|
25248
|
-
async function readAvdConfig(avdName) {
|
|
25249
|
-
const path = await findAvdConfigPath(avdName);
|
|
25250
|
-
if (!path)
|
|
25251
|
-
return { api: null, deviceName: null };
|
|
25252
|
-
try {
|
|
25253
|
-
const text2 = await readFile(path, "utf8");
|
|
25254
|
-
const imageLine = text2.split(`
|
|
25255
|
-
`).find((line) => line.includes("system-images") && line.includes("android-"));
|
|
25256
|
-
const apiMatch = imageLine?.match(/android-([\d.]+)/);
|
|
25257
|
-
const deviceLine = text2.split(`
|
|
25258
|
-
`).find((entry) => entry.startsWith("hw.device.name="));
|
|
25259
|
-
const deviceRaw = deviceLine?.slice("hw.device.name=".length).trim();
|
|
25260
|
-
return {
|
|
25261
|
-
api: apiMatch?.[1] ?? null,
|
|
25262
|
-
deviceName: humanizeModel(deviceRaw) ?? null
|
|
25263
|
-
};
|
|
25264
|
-
} catch {
|
|
25265
|
-
return { api: null, deviceName: null };
|
|
25266
|
-
}
|
|
25267
|
-
}
|
|
25268
|
-
async function adbAvdName(adb, serial) {
|
|
25269
|
-
const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "emu", "avd", "name"]);
|
|
25270
|
-
if (exitCode !== 0)
|
|
25271
|
-
return null;
|
|
25272
|
-
const name = stdout.trim().split(`
|
|
25273
|
-
`)[0]?.trim();
|
|
25274
|
-
return name || null;
|
|
25275
|
-
}
|
|
25276
|
-
async function adbGetProp(adb, serial, prop) {
|
|
25277
|
-
const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "shell", "getprop", prop]);
|
|
25278
|
-
if (exitCode !== 0)
|
|
25279
|
-
return null;
|
|
25280
|
-
const value = stdout.trim();
|
|
25281
|
-
return value || null;
|
|
25282
|
-
}
|
|
25283
|
-
async function listAndroidAdbDevices(adb) {
|
|
25284
|
-
const { stdout, exitCode } = await runCommand3([adb, "devices", "-l"]);
|
|
25285
|
-
if (exitCode !== 0) {
|
|
25286
|
-
return { physical: [], runningEmulators: [], runningAvdNames: new Set };
|
|
25287
|
-
}
|
|
25288
|
-
const entries = parseAdbDevices(stdout);
|
|
25289
|
-
const physical = [];
|
|
25290
|
-
const runningEmulators = [];
|
|
25291
|
-
const runningAvdNames = new Set;
|
|
25292
|
-
for (const entry of entries) {
|
|
25293
|
-
const isEmulator = entry.serial.startsWith("emulator-") || entry.product === "sdk_gphone";
|
|
25294
|
-
if (isEmulator) {
|
|
25295
|
-
const avdName = await adbAvdName(adb, entry.serial);
|
|
25296
|
-
if (avdName)
|
|
25297
|
-
runningAvdNames.add(avdName);
|
|
25298
|
-
const avdConfig = avdName ? await readAvdConfig(avdName) : null;
|
|
25299
|
-
const release2 = await adbGetProp(adb, entry.serial, "ro.build.version.release") ?? avdConfig?.api ?? null;
|
|
25300
|
-
const model2 = humanizeModel(entry.model) ?? avdConfig?.deviceName ?? (avdName ? humanizeAvdName(avdName) : entry.serial);
|
|
25301
|
-
runningEmulators.push({
|
|
25302
|
-
id: avdName ?? entry.serial,
|
|
25303
|
-
name: model2,
|
|
25304
|
-
osVersion: formatAndroidVersion(release2 ?? ""),
|
|
25305
|
-
platform: "android",
|
|
25306
|
-
kind: "emulator",
|
|
25307
|
-
state: entry.state === "device" ? "online" : entry.state,
|
|
25308
|
-
model: avdName ?? entry.model
|
|
25309
|
-
});
|
|
25310
|
-
continue;
|
|
25311
|
-
}
|
|
25312
|
-
const release = await adbGetProp(adb, entry.serial, "ro.build.version.release");
|
|
25313
|
-
const model = humanizeModel(entry.model) ?? await adbGetProp(adb, entry.serial, "ro.product.model") ?? entry.serial;
|
|
25314
|
-
physical.push({
|
|
25315
|
-
id: entry.serial,
|
|
25316
|
-
name: model,
|
|
25317
|
-
osVersion: formatAndroidVersion(release ?? ""),
|
|
25318
|
-
platform: "android",
|
|
25319
|
-
kind: "physical",
|
|
25320
|
-
state: entry.state,
|
|
25321
|
-
model: entry.model
|
|
25322
|
-
});
|
|
25323
|
-
}
|
|
25324
|
-
return { physical, runningEmulators, runningAvdNames };
|
|
25325
|
-
}
|
|
25326
|
-
async function listAndroidAvds(emulatorBin, runningAvdNames, includeUnavailable) {
|
|
25327
|
-
const { stdout, exitCode } = await runCommand3([emulatorBin, "-list-avds"]);
|
|
25328
|
-
if (exitCode !== 0)
|
|
25329
|
-
return [];
|
|
25330
|
-
const names = stdout.split(`
|
|
25331
|
-
`).map((line) => line.trim()).filter(Boolean);
|
|
25332
|
-
const devices = [];
|
|
25333
|
-
for (const avdName of names) {
|
|
25334
|
-
const isRunning = runningAvdNames.has(avdName);
|
|
25335
|
-
if (!includeUnavailable && !isRunning)
|
|
25336
|
-
continue;
|
|
25337
|
-
if (isRunning)
|
|
25338
|
-
continue;
|
|
25339
|
-
const { api, deviceName } = await readAvdConfig(avdName);
|
|
25340
|
-
devices.push({
|
|
25341
|
-
id: avdName,
|
|
25342
|
-
name: deviceName ?? humanizeAvdName(avdName),
|
|
25343
|
-
osVersion: formatAndroidVersion(api ?? ""),
|
|
25344
|
-
platform: "android",
|
|
25345
|
-
kind: "emulator",
|
|
25346
|
-
state: "offline",
|
|
25347
|
-
model: avdName
|
|
25348
|
-
});
|
|
25349
|
-
}
|
|
25350
|
-
return devices;
|
|
25351
|
-
}
|
|
25352
|
-
async function listAndroidDevices(options = {}) {
|
|
25353
|
-
const includeUnavailable = options.includeUnavailable ?? true;
|
|
25354
|
-
const [adb, emulatorBin] = await Promise.all([resolveAdb(), resolveEmulator()]);
|
|
25355
|
-
const adbResult = adb ? await listAndroidAdbDevices(adb) : { physical: [], runningEmulators: [], runningAvdNames: new Set };
|
|
25356
|
-
const offlineAvds = emulatorBin != null ? await listAndroidAvds(emulatorBin, adbResult.runningAvdNames, includeUnavailable) : [];
|
|
25357
|
-
const physical = includeUnavailable ? adbResult.physical : adbResult.physical.filter((device) => device.state === "device");
|
|
25358
|
-
const emulators = [...adbResult.runningEmulators, ...offlineAvds].sort((a, b) => {
|
|
25359
|
-
const aOnline = a.state === "online" ? 0 : 1;
|
|
25360
|
-
const bOnline = b.state === "online" ? 0 : 1;
|
|
25361
|
-
if (aOnline !== bOnline)
|
|
25362
|
-
return aOnline - bOnline;
|
|
25363
|
-
return a.name.localeCompare(b.name);
|
|
25364
|
-
});
|
|
25365
|
-
return [...physical, ...emulators];
|
|
25366
|
-
}
|
|
25367
|
-
async function listDevices(platform, options = {}) {
|
|
25368
|
-
if (platform === "ios")
|
|
25369
|
-
return listIosDevices(options);
|
|
25370
|
-
return listAndroidDevices(options);
|
|
25371
|
-
}
|
|
25372
|
-
|
|
25373
25484
|
// src/interfaces/http/devices.ts
|
|
25374
25485
|
function createDevicesRoutes() {
|
|
25375
25486
|
const app = new Hono2;
|
|
@@ -97429,6 +97540,10 @@ var defaultClock = {
|
|
|
97429
97540
|
};
|
|
97430
97541
|
async function executeScriptCase(deps) {
|
|
97431
97542
|
const perform = deps.performAction ?? performAction;
|
|
97543
|
+
const readScreen = deps.readScreen ?? (async (session) => {
|
|
97544
|
+
const screen = await getScreen(session, { full: false });
|
|
97545
|
+
return { elements: screen.elements };
|
|
97546
|
+
});
|
|
97432
97547
|
const clock = deps.clock ?? defaultClock;
|
|
97433
97548
|
const settleMs = deps.settleMs ?? POST_ACTION_SETTLE_MS;
|
|
97434
97549
|
let stepIdx = 0;
|
|
@@ -97446,7 +97561,16 @@ async function executeScriptCase(deps) {
|
|
|
97446
97561
|
return "cancelled";
|
|
97447
97562
|
}
|
|
97448
97563
|
if (action.type === "tap") {
|
|
97449
|
-
|
|
97564
|
+
const tapBody = { kind: "tap" };
|
|
97565
|
+
if (action.label)
|
|
97566
|
+
tapBody.label = action.label;
|
|
97567
|
+
if (action.id)
|
|
97568
|
+
tapBody.id = action.id;
|
|
97569
|
+
if (action.x != null)
|
|
97570
|
+
tapBody.x = action.x;
|
|
97571
|
+
if (action.y != null)
|
|
97572
|
+
tapBody.y = action.y;
|
|
97573
|
+
await perform(deps.session, tapBody);
|
|
97450
97574
|
await clock.sleep(settleMs);
|
|
97451
97575
|
await deps.appendStep({
|
|
97452
97576
|
idx: stepIdx,
|
|
@@ -97454,13 +97578,15 @@ async function executeScriptCase(deps) {
|
|
|
97454
97578
|
type: "tap",
|
|
97455
97579
|
x: action.x,
|
|
97456
97580
|
y: action.y,
|
|
97581
|
+
label: action.label,
|
|
97582
|
+
id: action.id,
|
|
97457
97583
|
reason: action.reason ?? "Replayed saved script tap",
|
|
97458
97584
|
thoughts: "Replaying the saved script without calling the AI agent."
|
|
97459
97585
|
},
|
|
97460
97586
|
screenshotUri: shot.path,
|
|
97461
97587
|
ok: true,
|
|
97462
97588
|
latencyMs,
|
|
97463
|
-
detail: action.reason ?? null
|
|
97589
|
+
detail: action.reason ?? action.label ?? action.id ?? null
|
|
97464
97590
|
});
|
|
97465
97591
|
} else if (action.type === "type") {
|
|
97466
97592
|
await perform(deps.session, { kind: "input", text: action.text });
|
|
@@ -97478,6 +97604,40 @@ async function executeScriptCase(deps) {
|
|
|
97478
97604
|
latencyMs,
|
|
97479
97605
|
detail: action.reason ?? null
|
|
97480
97606
|
});
|
|
97607
|
+
} else if (action.type === "assert") {
|
|
97608
|
+
const timeoutMs = action.timeoutMs ?? 5000;
|
|
97609
|
+
const assertion = action.assertion;
|
|
97610
|
+
const deadline = clock.now() + timeoutMs;
|
|
97611
|
+
for (;; ) {
|
|
97612
|
+
if (deps.isAborted()) {
|
|
97613
|
+
return "cancelled";
|
|
97614
|
+
}
|
|
97615
|
+
const screen = await readScreen(deps.session);
|
|
97616
|
+
const found = screenHasText(screen.elements, action.text);
|
|
97617
|
+
if (assertion === "visible" && found)
|
|
97618
|
+
break;
|
|
97619
|
+
if (assertion === "not-visible" && !found)
|
|
97620
|
+
break;
|
|
97621
|
+
if (clock.now() >= deadline) {
|
|
97622
|
+
throw new Error(assertion === "visible" ? `Expected visible text not found within ${Math.round(timeoutMs / 1000)}s: ${action.text}` : `Unexpected text still visible after ${Math.round(timeoutMs / 1000)}s: ${action.text}`);
|
|
97623
|
+
}
|
|
97624
|
+
await clock.sleep(400);
|
|
97625
|
+
}
|
|
97626
|
+
await deps.appendStep({
|
|
97627
|
+
idx: stepIdx,
|
|
97628
|
+
action: {
|
|
97629
|
+
type: "assert",
|
|
97630
|
+
assertion,
|
|
97631
|
+
text: action.text,
|
|
97632
|
+
timeoutMs,
|
|
97633
|
+
reason: action.reason ?? `Assert ${assertion}: ${action.text}`,
|
|
97634
|
+
thoughts: "Replaying the saved script without calling the AI agent."
|
|
97635
|
+
},
|
|
97636
|
+
screenshotUri: shot.path,
|
|
97637
|
+
ok: true,
|
|
97638
|
+
latencyMs,
|
|
97639
|
+
detail: action.reason ?? `${assertion}: ${action.text}`
|
|
97640
|
+
});
|
|
97481
97641
|
} else {
|
|
97482
97642
|
const waitMs = Math.min(3000, Math.max(500, action.ms));
|
|
97483
97643
|
await clock.sleep(waitMs);
|
package/package.json
CHANGED